Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3fd4939
Initial commit to update descriptions and tags
irvinesunday Jan 31, 2023
40762cb
Second init for operations and tags
irvinesunday Jan 31, 2023
1bcc59b
Get operation ids
irvinesunday Jan 31, 2023
57fbd6a
Refactor out common code into a helper class for re-use
irvinesunday Feb 1, 2023
f7930ef
Fix tags; refactor; remove redundant code
irvinesunday Feb 1, 2023
7b4efdb
Updates release notes
irvinesunday Feb 1, 2023
b379821
Add SetTags operation for $count operations; update operationId
irvinesunday Mar 8, 2023
993a03e
Remove unnecessary usings
irvinesunday Mar 8, 2023
4eed44a
Add operation id for $count paths for OData type cast segments
irvinesunday Mar 8, 2023
8cc0aaa
Refactor variable name
irvinesunday Mar 8, 2023
aade254
Merge remote-tracking branch 'origin/master' into fix/is/odata-type-c…
irvinesunday Mar 8, 2023
f85e816
Bump up lib. version
irvinesunday Mar 9, 2023
10a9c62
Refactor out private method; update tag generation
irvinesunday Mar 9, 2023
8109875
Update tags generation
irvinesunday Mar 9, 2023
178191a
Create common reusable class
irvinesunday Mar 9, 2023
47ad99a
Generate complex property path tag name in a helper class
irvinesunday Mar 13, 2023
d4680b5
Generate tags for entity set $count paths
irvinesunday Mar 13, 2023
98612fd
Variables renaming
irvinesunday Mar 13, 2023
6ef2f4b
Update integration and unit tests
irvinesunday Mar 13, 2023
abb3c34
Refactor methods that generate operation ids and tags
irvinesunday Mar 14, 2023
ca1e12c
Fix formatting; use methods in helper class to generate tags
irvinesunday Mar 14, 2023
a8785e1
Update unit and integration tests
irvinesunday Mar 15, 2023
490cf70
Variable renaming; fix type cast and complex types operation ids
irvinesunday Mar 15, 2023
d79cdfd
Merge branch 'master' into fix/is/odata-type-cast-opIds
irvinesunday Mar 28, 2023
0c847cf
Bump up version
irvinesunday Mar 28, 2023
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
272 changes: 264 additions & 8 deletions src/Microsoft.OpenApi.OData.Reader/Common/EdmModelHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// ------------------------------------------------------------
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// ------------------------------------------------------------
Expand All @@ -8,6 +8,7 @@
using System.Linq;
using Microsoft.OData.Edm;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.OData.Edm;
using Microsoft.OpenApi.OData.Vocabulary.Capabilities;

namespace Microsoft.OpenApi.OData.Common
Expand All @@ -22,7 +23,7 @@ internal static OpenApiSchema GetDerivedTypesReferenceSchema(IEdmStructuredType
{
Utils.CheckArgumentNull(structuredType, nameof(structuredType));
Utils.CheckArgumentNull(edmModel, nameof(edmModel));
if(structuredType is not IEdmSchemaElement schemaElement) throw new ArgumentException("The type is not a schema element.", nameof(structuredType));
if (structuredType is not IEdmSchemaElement schemaElement) throw new ArgumentException("The type is not a schema element.", nameof(structuredType));

IEnumerable<IEdmSchemaElement> derivedTypes = edmModel.FindAllDerivedTypes(structuredType).OfType<IEdmSchemaElement>();

Expand All @@ -32,12 +33,12 @@ internal static OpenApiSchema GetDerivedTypesReferenceSchema(IEdmStructuredType
}

OpenApiSchema schema = new()
{
{
OneOf = new List<OpenApiSchema>()
};

OpenApiSchema baseTypeSchema = new()
{
{
UnresolvedReference = true,
Reference = new OpenApiReference
{
Expand All @@ -50,7 +51,7 @@ internal static OpenApiSchema GetDerivedTypesReferenceSchema(IEdmStructuredType
foreach (IEdmSchemaElement derivedType in derivedTypes)
{
OpenApiSchema derivedTypeSchema = new()
{
{
UnresolvedReference = true,
Reference = new OpenApiReference
{
Expand All @@ -62,14 +63,14 @@ internal static OpenApiSchema GetDerivedTypesReferenceSchema(IEdmStructuredType
};

return schema;
}
}

/// <summary>
/// Verifies whether the provided navigation restrictions allow for navigability of a navigation property.
/// </summary>
/// <param name="restrictionType">The <see cref="NavigationRestrictionsType"/>.</param>
/// <param name="restrictionProperty">The <see cref="NavigationPropertyRestriction"/>.</param>
/// <returns></returns>
/// <returns>true, if navigability is allowed, otherwise false.</returns>
internal static bool NavigationRestrictionsAllowsNavigability(
NavigationRestrictionsType restrictionType,
NavigationPropertyRestriction restrictionProperty)
Expand All @@ -83,7 +84,262 @@ internal static bool NavigationRestrictionsAllowsNavigability(
// if the individual has no navigability setting, use the global navigability setting
// Default navigability for all navigation properties of the annotation target.
// Individual navigation properties can override this value via `RestrictedProperties/Navigability`.
return restrictionProperty?.Navigability != null || restrictionType == null || restrictionType.IsNavigable;
return restrictionProperty?.Navigability != null || restrictionType == null || restrictionType.IsNavigable;
}

/// <summary>
/// Generates the operation id from a navigation property path.
/// </summary>
/// <param name="path">The target <see cref="ODataPath"/>.</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)
{
IList<string> items = RetrieveNavigationPropertyPathsOperationIdSegments(path);

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

int lastItemIndex = items.Count - 1;

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

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

/// <summary>
/// Generates the operation id from a complex property path.
/// </summary>
/// <param name="path">The target <see cref="ODataPath"/>.</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)
{
IList<string> items = RetrieveNavigationPropertyPathsOperationIdSegments(path);

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

ODataComplexPropertySegment lastSegment = path.Segments.Skip(1).OfType<ODataComplexPropertySegment>()?.Last();
Utils.CheckArgumentNull(lastSegment, nameof(lastSegment));

if (!string.IsNullOrEmpty(prefix))
{
items.Add(prefix + Utils.UpperFirstChar(lastSegment?.Identifier));
}
else
{
items.Add(Utils.UpperFirstChar(lastSegment?.Identifier));
}

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

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

IEdmNavigationSource navigationSource = (path.FirstSegment as ODataNavigationSourceSegment)?.NavigationSource;
Utils.CheckArgumentNull(navigationSource, nameof(navigationSource));

IList<string> items = new List<string>
{
navigationSource.Name
};

IEnumerable<ODataNavigationPropertySegment> navPropSegments = path.Segments.Skip(1).OfType<ODataNavigationPropertySegment>();
Utils.CheckArgumentNull(navPropSegments, nameof(navPropSegments));

foreach (var segment in navPropSegments)
{
if (segment == navPropSegments.Last())
{
items.Add(segment.NavigationProperty.Name);
break;
}
else
{
items.Add(segment.NavigationProperty.Name);
}
}

return items;
}

/// <summary>
/// Generates the tag name from a navigation property path.
/// </summary>
/// <param name="path">The target <see cref="ODataPath"/>.</param>
/// <param name="context">The <see cref="ODataContext"/>.</param>
/// <returns>The tag name generated from the given navigation property path.</returns>
internal static string GenerateNavigationPropertyPathTagName(ODataPath path, ODataContext context)
{
Utils.CheckArgumentNull(path, nameof(path));
Utils.CheckArgumentNull(context, nameof(context));

IEdmNavigationSource navigationSource = (path.FirstSegment as ODataNavigationSourceSegment)?.NavigationSource;

IList<string> items = new List<string>
{
navigationSource.Name
};

IEdmNavigationProperty navigationProperty = path.OfType<ODataNavigationPropertySegment>()?.Last()?.NavigationProperty;
Utils.CheckArgumentNull(navigationProperty, nameof(navigationProperty));

foreach (var segment in path.Segments.Skip(1).OfType<ODataNavigationPropertySegment>())
{
if (segment.NavigationProperty == navigationProperty)
{
items.Add(navigationProperty.ToEntityType().Name);
break;
}
else
{
if (items.Count >= context.Settings.TagDepth - 1)
{
items.Add(segment.NavigationProperty.ToEntityType().Name);
break;
}
else
{
items.Add(segment.NavigationProperty.Name);
}
}
}

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

/// <summary>
/// Generates the tag name from a complex property path.
/// </summary>
/// <param name="path">The target <see cref="ODataPath"/>.</param>
/// <param name="context">The <see cref="ODataContext"/>.</param>
/// <returns>The tag name generated from the given complex property path.</returns>
internal static string GenerateComplexPropertyPathTagName(ODataPath path, ODataContext context)
{
Utils.CheckArgumentNull(path, nameof(path));

// Get the segment before the last complex type segment
ODataComplexPropertySegment complexSegment = path.Segments.OfType<ODataComplexPropertySegment>()?.Last();
Utils.CheckArgumentNull(complexSegment, nameof(complexSegment));

int complexSegmentIndex = path.Segments.IndexOf(complexSegment);
ODataSegment preComplexSegment = path.Segments.ElementAt(complexSegmentIndex - 1);
string tagName = null;

if (preComplexSegment is ODataNavigationSourceSegment sourceSegment)
{
tagName = $"{sourceSegment.NavigationSource.Name}";
}
else if (preComplexSegment is ODataNavigationPropertySegment)
{
tagName = GenerateNavigationPropertyPathTagName(path, context);
}
else if (preComplexSegment is ODataKeySegment)
{
var thirdLastSegment = path.Segments.ElementAt(complexSegmentIndex - 2);
if (thirdLastSegment is ODataNavigationPropertySegment)
{
tagName = GenerateNavigationPropertyPathTagName(path, context);
}
else if (thirdLastSegment is ODataNavigationSourceSegment sourceSegment1)
{
tagName = $"{sourceSegment1.NavigationSource.Name}";
}
}

List<string> tagNameItems = tagName?.Split('.').ToList();

if (tagNameItems.Count < context.Settings.TagDepth)
{
tagNameItems.Add(complexSegment.ComplexType.Name);
}

return string.Join(".", tagNameItems);
}

/// <summary>
/// Generates the operation id prefix from an OData type cast path.
/// </summary>
/// <param name="path">The target <see cref="ODataPath"/>.</param>
/// <returns>The operation id prefix generated from the OData type cast path.</returns>
internal static string GenerateODataTypeCastPathOperationIdPrefix(ODataPath path)
{
// Get the segment before the last OData type cast segment
ODataTypeCastSegment typeCastSegment = path.Segments.OfType<ODataTypeCastSegment>()?.Last();
Utils.CheckArgumentNull(typeCastSegment, nameof(typeCastSegment));

int typeCastSegmentIndex = path.Segments.IndexOf(typeCastSegment);

// The segment 1 place before the last OData type cast segment
ODataSegment secondLastSegment = path.Segments.ElementAt(typeCastSegmentIndex - 1);

bool isIndexedCollValuedNavProp = false;
if (secondLastSegment is ODataKeySegment)
{
// The segment 2 places before the last OData type cast segment
ODataSegment thirdLastSegment = path.Segments.ElementAt(typeCastSegmentIndex - 2);
if (thirdLastSegment is ODataNavigationPropertySegment)
{
isIndexedCollValuedNavProp = true;
}
}

ODataNavigationSourceSegment navigationSourceSegment = path.FirstSegment as ODataNavigationSourceSegment;
IEdmSingleton singleton = navigationSourceSegment?.NavigationSource as IEdmSingleton;
IEdmEntitySet entitySet = navigationSourceSegment?.NavigationSource as IEdmEntitySet;

string operationId = null;
if (secondLastSegment is ODataComplexPropertySegment complexSegment)
{
string listOrGet = complexSegment.Property.Type.IsCollection() ? "List" : "Get";
operationId = GenerateComplexPropertyPathOperationId(path, listOrGet);
}
else if (secondLastSegment as ODataNavigationPropertySegment is not null || isIndexedCollValuedNavProp)
{
string prefix = "Get";
if (!isIndexedCollValuedNavProp &&
(secondLastSegment as ODataNavigationPropertySegment)?.NavigationProperty.TargetMultiplicity() == EdmMultiplicity.Many)
{
prefix = "List";
}

operationId = GenerateNavigationPropertyPathOperationId(path, prefix);
}
else if (secondLastSegment is ODataKeySegment keySegment && !isIndexedCollValuedNavProp)
{
string entityTypeName = keySegment.EntityType.Name;
string operationName = $"Get{Utils.UpperFirstChar(entityTypeName)}";
if (keySegment.IsAlternateKey)
{
string alternateKeyName = string.Join("", keySegment.Identifier.Split(',').Select(static x => Utils.UpperFirstChar(x)));
operationName = $"{operationName}By{alternateKeyName}";
}
operationId = (entitySet != null) ? entitySet.Name : singleton.Name;
operationId += $".{entityTypeName}.{operationName}";
}
else if (secondLastSegment is ODataNavigationSourceSegment)
{
operationId = (entitySet != null)
? entitySet.Name + "." + entitySet.EntityType().Name + ".List" + Utils.UpperFirstChar(entitySet.EntityType().Name)
: singleton.Name + "." + singleton.EntityType().Name + ".Get" + Utils.UpperFirstChar(singleton.EntityType().Name);
}

return operationId;
}
}
}
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.3.0-preview4</Version>
<Version>1.3.0</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,6 +28,7 @@
- Return response status code 2XX for PUT operations of stream properties when UseSuccessStatusCodeRange is enabled #310
- Adds $value segment to paths with entity types with base types with HasStream=true #314
- Uses SemVerVersion in place of Version to Get or Set the metadata version in the OpenAPI document #346
- Resolves operationId and tag names for OData cast paths #324
</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 @@ -3,17 +3,41 @@
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// ------------------------------------------------------------

using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.OData.Common;
using Microsoft.OpenApi.OData.Edm;

namespace Microsoft.OpenApi.OData.Operation;

internal abstract class ComplexPropertyBaseOperationHandler : OperationHandler
{
/// <inheritdoc/>
protected ODataComplexPropertySegment ComplexPropertySegment;

/// <inheritdoc/>
protected override void Initialize(ODataContext context, ODataPath path)
{
ComplexPropertySegment = path.LastSegment as ODataComplexPropertySegment ?? throw Error.ArgumentNull(nameof(path));
}
protected ODataComplexPropertySegment ComplexPropertySegment;

/// <inheritdoc/>
protected override void SetTags(OpenApiOperation operation)
{
string tagName = EdmModelHelper.GenerateComplexPropertyPathTagName(Path, Context);

if (!string.IsNullOrEmpty(tagName))
{
OpenApiTag tag = new()
{
Name = tagName
};

tag.Extensions.Add(Constants.xMsTocType, new OpenApiString("page"));
operation.Tags.Add(tag);

Context.AppendTag(tag);
}

base.SetTags(operation);
}
}
Loading