Skip to content

Commit 7942102

Browse files
authored
Resolves operationId and tag names for OData cast paths (#338)
* Initial commit to update descriptions and tags * Second init for operations and tags * Get operation ids * Refactor out common code into a helper class for re-use * Fix tags; refactor; remove redundant code * Updates release notes * Add SetTags operation for $count operations; update operationId * Remove unnecessary usings * Add operation id for $count paths for OData type cast segments * Refactor variable name * Bump up lib. version * Refactor out private method; update tag generation * Update tags generation * Create common reusable class * Generate complex property path tag name in a helper class * Generate tags for entity set $count paths * Variables renaming * Update integration and unit tests * Refactor methods that generate operation ids and tags * Fix formatting; use methods in helper class to generate tags * Update unit and integration tests * Variable renaming; fix type cast and complex types operation ids * Bump up version
1 parent 793abf8 commit 7942102

27 files changed

+3101
-1221
lines changed

src/Microsoft.OpenApi.OData.Reader/Common/EdmModelHelper.cs

Lines changed: 264 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ------------------------------------------------------------
1+
// ------------------------------------------------------------
22
// Copyright (c) Microsoft Corporation. All rights reserved.
33
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
44
// ------------------------------------------------------------
@@ -8,6 +8,7 @@
88
using System.Linq;
99
using Microsoft.OData.Edm;
1010
using Microsoft.OpenApi.Models;
11+
using Microsoft.OpenApi.OData.Edm;
1112
using Microsoft.OpenApi.OData.Vocabulary.Capabilities;
1213

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

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

@@ -32,12 +33,12 @@ internal static OpenApiSchema GetDerivedTypesReferenceSchema(IEdmStructuredType
3233
}
3334

3435
OpenApiSchema schema = new()
35-
{
36+
{
3637
OneOf = new List<OpenApiSchema>()
3738
};
3839

3940
OpenApiSchema baseTypeSchema = new()
40-
{
41+
{
4142
UnresolvedReference = true,
4243
Reference = new OpenApiReference
4344
{
@@ -50,7 +51,7 @@ internal static OpenApiSchema GetDerivedTypesReferenceSchema(IEdmStructuredType
5051
foreach (IEdmSchemaElement derivedType in derivedTypes)
5152
{
5253
OpenApiSchema derivedTypeSchema = new()
53-
{
54+
{
5455
UnresolvedReference = true,
5556
Reference = new OpenApiReference
5657
{
@@ -62,14 +63,14 @@ internal static OpenApiSchema GetDerivedTypesReferenceSchema(IEdmStructuredType
6263
};
6364

6465
return schema;
65-
}
66+
}
6667

6768
/// <summary>
6869
/// Verifies whether the provided navigation restrictions allow for navigability of a navigation property.
6970
/// </summary>
7071
/// <param name="restrictionType">The <see cref="NavigationRestrictionsType"/>.</param>
7172
/// <param name="restrictionProperty">The <see cref="NavigationPropertyRestriction"/>.</param>
72-
/// <returns></returns>
73+
/// <returns>true, if navigability is allowed, otherwise false.</returns>
7374
internal static bool NavigationRestrictionsAllowsNavigability(
7475
NavigationRestrictionsType restrictionType,
7576
NavigationPropertyRestriction restrictionProperty)
@@ -83,7 +84,262 @@ internal static bool NavigationRestrictionsAllowsNavigability(
8384
// if the individual has no navigability setting, use the global navigability setting
8485
// Default navigability for all navigation properties of the annotation target.
8586
// Individual navigation properties can override this value via `RestrictedProperties/Navigability`.
86-
return restrictionProperty?.Navigability != null || restrictionType == null || restrictionType.IsNavigable;
87+
return restrictionProperty?.Navigability != null || restrictionType == null || restrictionType.IsNavigable;
88+
}
89+
90+
/// <summary>
91+
/// Generates the operation id from a navigation property path.
92+
/// </summary>
93+
/// <param name="path">The target <see cref="ODataPath"/>.</param>
94+
/// <param name="prefix">Optional: Identifier indicating whether it is a collection-valued non-indexed or single-valued navigation property.</param>
95+
/// <returns>The operation id generated from the given navigation property path.</returns>
96+
internal static string GenerateNavigationPropertyPathOperationId(ODataPath path, string prefix = null)
97+
{
98+
IList<string> items = RetrieveNavigationPropertyPathsOperationIdSegments(path);
99+
100+
if (!items.Any())
101+
return null;
102+
103+
int lastItemIndex = items.Count - 1;
104+
105+
if (!string.IsNullOrEmpty(prefix))
106+
{
107+
items[lastItemIndex] = prefix + Utils.UpperFirstChar(items.Last());
108+
}
109+
else
110+
{
111+
items[lastItemIndex] = Utils.UpperFirstChar(items.Last());
112+
}
113+
114+
return string.Join(".", items);
115+
}
116+
117+
/// <summary>
118+
/// Generates the operation id from a complex property path.
119+
/// </summary>
120+
/// <param name="path">The target <see cref="ODataPath"/>.</param>
121+
/// <param name="prefix">Optional: Identifier indicating whether it is a collection-valued or single-valued complex property.</param>
122+
/// <returns>The operation id generated from the given complex property path.</returns>
123+
internal static string GenerateComplexPropertyPathOperationId(ODataPath path, string prefix = null)
124+
{
125+
IList<string> items = RetrieveNavigationPropertyPathsOperationIdSegments(path);
126+
127+
if (!items.Any())
128+
return null;
129+
130+
ODataComplexPropertySegment lastSegment = path.Segments.Skip(1).OfType<ODataComplexPropertySegment>()?.Last();
131+
Utils.CheckArgumentNull(lastSegment, nameof(lastSegment));
132+
133+
if (!string.IsNullOrEmpty(prefix))
134+
{
135+
items.Add(prefix + Utils.UpperFirstChar(lastSegment?.Identifier));
136+
}
137+
else
138+
{
139+
items.Add(Utils.UpperFirstChar(lastSegment?.Identifier));
140+
}
141+
142+
return string.Join(".", items);
143+
}
144+
145+
/// <summary>
146+
/// Retrieves the segments of an operation id generated from a navigation property path.
147+
/// </summary>
148+
/// <param name="path">The target <see cref="ODataPath"/>.</param>
149+
/// <returns>The segments of an operation id generated from the given navigation property path.</returns>
150+
internal static IList<string> RetrieveNavigationPropertyPathsOperationIdSegments(ODataPath path)
151+
{
152+
Utils.CheckArgumentNull(path, nameof(path));
153+
154+
IEdmNavigationSource navigationSource = (path.FirstSegment as ODataNavigationSourceSegment)?.NavigationSource;
155+
Utils.CheckArgumentNull(navigationSource, nameof(navigationSource));
156+
157+
IList<string> items = new List<string>
158+
{
159+
navigationSource.Name
160+
};
161+
162+
IEnumerable<ODataNavigationPropertySegment> navPropSegments = path.Segments.Skip(1).OfType<ODataNavigationPropertySegment>();
163+
Utils.CheckArgumentNull(navPropSegments, nameof(navPropSegments));
164+
165+
foreach (var segment in navPropSegments)
166+
{
167+
if (segment == navPropSegments.Last())
168+
{
169+
items.Add(segment.NavigationProperty.Name);
170+
break;
171+
}
172+
else
173+
{
174+
items.Add(segment.NavigationProperty.Name);
175+
}
176+
}
177+
178+
return items;
179+
}
180+
181+
/// <summary>
182+
/// Generates the tag name from a navigation property path.
183+
/// </summary>
184+
/// <param name="path">The target <see cref="ODataPath"/>.</param>
185+
/// <param name="context">The <see cref="ODataContext"/>.</param>
186+
/// <returns>The tag name generated from the given navigation property path.</returns>
187+
internal static string GenerateNavigationPropertyPathTagName(ODataPath path, ODataContext context)
188+
{
189+
Utils.CheckArgumentNull(path, nameof(path));
190+
Utils.CheckArgumentNull(context, nameof(context));
191+
192+
IEdmNavigationSource navigationSource = (path.FirstSegment as ODataNavigationSourceSegment)?.NavigationSource;
193+
194+
IList<string> items = new List<string>
195+
{
196+
navigationSource.Name
197+
};
198+
199+
IEdmNavigationProperty navigationProperty = path.OfType<ODataNavigationPropertySegment>()?.Last()?.NavigationProperty;
200+
Utils.CheckArgumentNull(navigationProperty, nameof(navigationProperty));
201+
202+
foreach (var segment in path.Segments.Skip(1).OfType<ODataNavigationPropertySegment>())
203+
{
204+
if (segment.NavigationProperty == navigationProperty)
205+
{
206+
items.Add(navigationProperty.ToEntityType().Name);
207+
break;
208+
}
209+
else
210+
{
211+
if (items.Count >= context.Settings.TagDepth - 1)
212+
{
213+
items.Add(segment.NavigationProperty.ToEntityType().Name);
214+
break;
215+
}
216+
else
217+
{
218+
items.Add(segment.NavigationProperty.Name);
219+
}
220+
}
221+
}
222+
223+
return string.Join(".", items);
224+
}
225+
226+
/// <summary>
227+
/// Generates the tag name from a complex property path.
228+
/// </summary>
229+
/// <param name="path">The target <see cref="ODataPath"/>.</param>
230+
/// <param name="context">The <see cref="ODataContext"/>.</param>
231+
/// <returns>The tag name generated from the given complex property path.</returns>
232+
internal static string GenerateComplexPropertyPathTagName(ODataPath path, ODataContext context)
233+
{
234+
Utils.CheckArgumentNull(path, nameof(path));
235+
236+
// Get the segment before the last complex type segment
237+
ODataComplexPropertySegment complexSegment = path.Segments.OfType<ODataComplexPropertySegment>()?.Last();
238+
Utils.CheckArgumentNull(complexSegment, nameof(complexSegment));
239+
240+
int complexSegmentIndex = path.Segments.IndexOf(complexSegment);
241+
ODataSegment preComplexSegment = path.Segments.ElementAt(complexSegmentIndex - 1);
242+
string tagName = null;
243+
244+
if (preComplexSegment is ODataNavigationSourceSegment sourceSegment)
245+
{
246+
tagName = $"{sourceSegment.NavigationSource.Name}";
247+
}
248+
else if (preComplexSegment is ODataNavigationPropertySegment)
249+
{
250+
tagName = GenerateNavigationPropertyPathTagName(path, context);
251+
}
252+
else if (preComplexSegment is ODataKeySegment)
253+
{
254+
var thirdLastSegment = path.Segments.ElementAt(complexSegmentIndex - 2);
255+
if (thirdLastSegment is ODataNavigationPropertySegment)
256+
{
257+
tagName = GenerateNavigationPropertyPathTagName(path, context);
258+
}
259+
else if (thirdLastSegment is ODataNavigationSourceSegment sourceSegment1)
260+
{
261+
tagName = $"{sourceSegment1.NavigationSource.Name}";
262+
}
263+
}
264+
265+
List<string> tagNameItems = tagName?.Split('.').ToList();
266+
267+
if (tagNameItems.Count < context.Settings.TagDepth)
268+
{
269+
tagNameItems.Add(complexSegment.ComplexType.Name);
270+
}
271+
272+
return string.Join(".", tagNameItems);
273+
}
274+
275+
/// <summary>
276+
/// Generates the operation id prefix from an OData type cast path.
277+
/// </summary>
278+
/// <param name="path">The target <see cref="ODataPath"/>.</param>
279+
/// <returns>The operation id prefix generated from the OData type cast path.</returns>
280+
internal static string GenerateODataTypeCastPathOperationIdPrefix(ODataPath path)
281+
{
282+
// Get the segment before the last OData type cast segment
283+
ODataTypeCastSegment typeCastSegment = path.Segments.OfType<ODataTypeCastSegment>()?.Last();
284+
Utils.CheckArgumentNull(typeCastSegment, nameof(typeCastSegment));
285+
286+
int typeCastSegmentIndex = path.Segments.IndexOf(typeCastSegment);
287+
288+
// The segment 1 place before the last OData type cast segment
289+
ODataSegment secondLastSegment = path.Segments.ElementAt(typeCastSegmentIndex - 1);
290+
291+
bool isIndexedCollValuedNavProp = false;
292+
if (secondLastSegment is ODataKeySegment)
293+
{
294+
// The segment 2 places before the last OData type cast segment
295+
ODataSegment thirdLastSegment = path.Segments.ElementAt(typeCastSegmentIndex - 2);
296+
if (thirdLastSegment is ODataNavigationPropertySegment)
297+
{
298+
isIndexedCollValuedNavProp = true;
299+
}
300+
}
301+
302+
ODataNavigationSourceSegment navigationSourceSegment = path.FirstSegment as ODataNavigationSourceSegment;
303+
IEdmSingleton singleton = navigationSourceSegment?.NavigationSource as IEdmSingleton;
304+
IEdmEntitySet entitySet = navigationSourceSegment?.NavigationSource as IEdmEntitySet;
305+
306+
string operationId = null;
307+
if (secondLastSegment is ODataComplexPropertySegment complexSegment)
308+
{
309+
string listOrGet = complexSegment.Property.Type.IsCollection() ? "List" : "Get";
310+
operationId = GenerateComplexPropertyPathOperationId(path, listOrGet);
311+
}
312+
else if (secondLastSegment as ODataNavigationPropertySegment is not null || isIndexedCollValuedNavProp)
313+
{
314+
string prefix = "Get";
315+
if (!isIndexedCollValuedNavProp &&
316+
(secondLastSegment as ODataNavigationPropertySegment)?.NavigationProperty.TargetMultiplicity() == EdmMultiplicity.Many)
317+
{
318+
prefix = "List";
319+
}
320+
321+
operationId = GenerateNavigationPropertyPathOperationId(path, prefix);
322+
}
323+
else if (secondLastSegment is ODataKeySegment keySegment && !isIndexedCollValuedNavProp)
324+
{
325+
string entityTypeName = keySegment.EntityType.Name;
326+
string operationName = $"Get{Utils.UpperFirstChar(entityTypeName)}";
327+
if (keySegment.IsAlternateKey)
328+
{
329+
string alternateKeyName = string.Join("", keySegment.Identifier.Split(',').Select(static x => Utils.UpperFirstChar(x)));
330+
operationName = $"{operationName}By{alternateKeyName}";
331+
}
332+
operationId = (entitySet != null) ? entitySet.Name : singleton.Name;
333+
operationId += $".{entityTypeName}.{operationName}";
334+
}
335+
else if (secondLastSegment is ODataNavigationSourceSegment)
336+
{
337+
operationId = (entitySet != null)
338+
? entitySet.Name + "." + entitySet.EntityType().Name + ".List" + Utils.UpperFirstChar(entitySet.EntityType().Name)
339+
: singleton.Name + "." + singleton.EntityType().Name + ".Get" + Utils.UpperFirstChar(singleton.EntityType().Name);
340+
}
341+
342+
return operationId;
87343
}
88344
}
89345
}

src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<TargetFrameworks>netstandard2.0</TargetFrameworks>
1616
<PackageId>Microsoft.OpenApi.OData</PackageId>
1717
<SignAssembly>true</SignAssembly>
18-
<Version>1.3.0-preview4</Version>
18+
<Version>1.3.0</Version>
1919
<Description>This package contains the codes you need to convert OData CSDL to Open API Document of Model.</Description>
2020
<Copyright>© Microsoft Corporation. All rights reserved.</Copyright>
2121
<PackageTags>Microsoft OpenApi OData EDM</PackageTags>
@@ -28,6 +28,7 @@
2828
- Return response status code 2XX for PUT operations of stream properties when UseSuccessStatusCodeRange is enabled #310
2929
- Adds $value segment to paths with entity types with base types with HasStream=true #314
3030
- Uses SemVerVersion in place of Version to Get or Set the metadata version in the OpenAPI document #346
31+
- Resolves operationId and tag names for OData cast paths #324
3132
</PackageReleaseNotes>
3233
<AssemblyName>Microsoft.OpenApi.OData.Reader</AssemblyName>
3334
<AssemblyOriginatorKeyFile>..\..\tool\Microsoft.OpenApi.OData.snk</AssemblyOriginatorKeyFile>

src/Microsoft.OpenApi.OData.Reader/Operation/ComplexPropertyBaseOperationHandler.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,41 @@
33
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
44
// ------------------------------------------------------------
55

6+
using Microsoft.OpenApi.Any;
7+
using Microsoft.OpenApi.Models;
68
using Microsoft.OpenApi.OData.Common;
79
using Microsoft.OpenApi.OData.Edm;
810

911
namespace Microsoft.OpenApi.OData.Operation;
1012

1113
internal abstract class ComplexPropertyBaseOperationHandler : OperationHandler
1214
{
13-
/// <inheritdoc/>
15+
protected ODataComplexPropertySegment ComplexPropertySegment;
16+
17+
/// <inheritdoc/>
1418
protected override void Initialize(ODataContext context, ODataPath path)
1519
{
1620
ComplexPropertySegment = path.LastSegment as ODataComplexPropertySegment ?? throw Error.ArgumentNull(nameof(path));
1721
}
18-
protected ODataComplexPropertySegment ComplexPropertySegment;
22+
23+
/// <inheritdoc/>
24+
protected override void SetTags(OpenApiOperation operation)
25+
{
26+
string tagName = EdmModelHelper.GenerateComplexPropertyPathTagName(Path, Context);
27+
28+
if (!string.IsNullOrEmpty(tagName))
29+
{
30+
OpenApiTag tag = new()
31+
{
32+
Name = tagName
33+
};
34+
35+
tag.Extensions.Add(Constants.xMsTocType, new OpenApiString("page"));
36+
operation.Tags.Add(tag);
37+
38+
Context.AppendTag(tag);
39+
}
40+
41+
base.SetTags(operation);
42+
}
1943
}

0 commit comments

Comments
 (0)