Compare commits

...

13 commits

Author SHA1 Message Date
Vincent Biret 26eabeceae
- adds support for singleton downcast
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
2021-11-26 14:10:08 -05:00
Vincent Biret 85fda323ff
- adds support for single navigation property cast
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
2021-11-26 13:59:17 -05:00
Vincent Biret dae89b6da7
- adds support for casting after key segment
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
2021-11-26 12:52:10 -05:00
Vincent Biret d0b3e2079f
- adds support for entity set down cast
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
2021-11-25 15:00:15 -05:00
Vincent Biret 4ee8e9e5eb
- adds unit test for Cast operation handler & cast path item handler
- adds support for query parameters when in navigation properties

Signed-off-by: Vincent Biret <vibiret@microsoft.com>
2021-11-25 14:26:44 -05:00
Vincent Biret 7eee372958
- bump language version for test project
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
2021-11-25 14:26:43 -05:00
Vincent Biret 4790d7067e
- code linting
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
2021-11-25 14:26:43 -05:00
Vincent Biret 9716b1d0bf
- draft implementation of odata cast segments 2021-11-25 14:26:42 -05:00
Vincent Biret b5b9e08908
- typo fixes
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
2021-11-25 14:26:42 -05:00
Vincent Biret 1bb0a3feca
- adds extension method to public api declaration 2021-11-25 14:26:11 -05:00
Vincent Biret faa8df1365
- fixes a bug where quotes were added to boool/number parameters
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
2021-11-24 12:54:06 -05:00
Vincent Biret f31925bd64
- reverts wrong test cases fro parameter quotes 2021-11-24 12:29:52 -05:00
Vincent Biret 9f6a60e5ad
- fixes #140 a bug where quotes would be missing in url templates 2021-11-24 12:02:25 -05:00
31 changed files with 963 additions and 142 deletions

View file

@ -0,0 +1,35 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// ------------------------------------------------------------
using Microsoft.OData.Edm;
namespace Microsoft.OpenApi.OData.Edm
{
/// <summary>
/// Extension methods for <see cref="IEdmType"/>
/// </summary>
public static class EdmTypeExtensions
{
/// <summary>
/// Determines wether a path parameter should be wrapped in quotes based on the type of the parameter.
/// </summary>
/// <param name="edmType">The type of the parameter.</param>
public static bool ShouldPathParameterBeQuoted(this IEdmType edmType)
{
if (edmType == null)
{
return false;
}
return edmType.TypeKind switch
{
EdmTypeKind.Enum => true,
EdmTypeKind.Primitive when edmType.IsString() || edmType.IsTemporal() => true,
_ => false,
};
}
}
}

View file

@ -77,13 +77,7 @@ namespace Microsoft.OpenApi.OData.Edm
}
else
{
IList<string> keyStrings = new List<string>();
foreach (var key in KeyMappings)
{
keyStrings.Add(key.Key + "={" + key.Value + "}");
}
return String.Join(",", keyStrings);
return string.Join(",", KeyMappings.Select(x => x.Key + "='{" + x.Value + "}'"));
}
}
@ -109,7 +103,8 @@ namespace Microsoft.OpenApi.OData.Edm
foreach (var keyProperty in keys)
{
string name = Utils.GetUniqueName(keyProperty.Name, parameters);
keyStrings.Add(keyProperty.Name + "={" + name + "}");
var quote = keyProperty.Type.Definition.ShouldPathParameterBeQuoted() ? "'" : string.Empty;
keyStrings.Add($"{keyProperty.Name}={quote}{{{name}}}{quote}");
}
return String.Join(",", keyStrings);

View file

@ -83,7 +83,8 @@ namespace Microsoft.OpenApi.OData.Edm
}
else
{
return p.Name + "={" + uniqueName + "}";
var quote = p.Type.Definition.ShouldPathParameterBeQuoted() ? "'" : string.Empty;
return $"{p.Name}={quote}{{{uniqueName}}}{quote}";
}
})));

View file

@ -148,7 +148,8 @@ namespace Microsoft.OpenApi.OData.Edm
}
else
{
return p.Name + "={" + uniqueName + "}";
var quote = p.Type.Definition.ShouldPathParameterBeQuoted() ? "'" : string.Empty;
return p.Name + $"={quote}{{{uniqueName}}}{quote}";
}
})));

View file

@ -289,13 +289,15 @@ namespace Microsoft.OpenApi.OData.Edm
{
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;
}
@ -315,22 +317,17 @@ namespace Microsoft.OpenApi.OData.Edm
{
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)
{
return ODataPathKind.Entity;

View file

@ -26,7 +26,7 @@ namespace Microsoft.OpenApi.OData.Edm
Singleton,
/// <summary>
/// Represents an operation (function or action) path, for example: ~/users/NS.findRooms(roomId={roomId})
/// Represents an operation (function or action) path, for example: ~/users/NS.findRooms(roomId='{roomId}')
/// </summary>
Operation,
@ -60,9 +60,14 @@ namespace Microsoft.OpenApi.OData.Edm
/// </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,
}
}

View file

@ -127,6 +127,7 @@ namespace Microsoft.OpenApi.OData.Edm
ODataPathKind kind = path.Kind;
switch(kind)
{
case ODataPathKind.TypeCast:
case ODataPathKind.DollarCount:
case ODataPathKind.Entity:
case ODataPathKind.EntitySet:
@ -189,6 +190,15 @@ namespace Microsoft.OpenApi.OData.Edm
if(count?.Countable ?? true)
CreateCountPath(path, convertSettings);
//TODO read the cast restrictions annotation
var derivedTypes = _model
.FindAllDerivedTypes(entitySet.EntityType())
.Where(x => x.TypeKind == EdmTypeKind.Entity)
.OfType<IEdmEntityType>()
.ToArray();
if(derivedTypes.Any())
CreateTypeCastPaths(path, convertSettings, derivedTypes);
path.Push(new ODataKeySegment(entityType));
AppendPath(path.Clone());
}
@ -285,19 +295,32 @@ namespace Microsoft.OpenApi.OData.Edm
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);
}
//TODO read the cast restrictions annotation
var derivedTypes = _model
.FindAllDerivedTypes(navigationProperty.DeclaringType)
.Where(x => x.TypeKind == EdmTypeKind.Entity)
.OfType<IEdmEntityType>()
.ToArray();
if(derivedTypes.Any())
CreateTypeCastPaths(currentPath, convertSettings, derivedTypes);
}
if (!navigationProperty.ContainsTarget)
{
// Non-Contained
// Single-Valued: ~/entityset/{key}/single-valued-Nav/$ref
// Collection-valued: ~/entityset/{key}/collection-valued-Nav/$ref?$id ={navKey}
// Collection-valued: ~/entityset/{key}/collection-valued-Nav/$ref?$id='{navKey}'
CreateRefPath(currentPath);
if (targetsMany)
@ -393,6 +416,26 @@ namespace Microsoft.OpenApi.OData.Edm
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="targetTypes">The target types to generate a path for.</param>
private void CreateTypeCastPaths(ODataPath currentPath, OpenApiConvertSettings convertSettings, params IEdmEntityType[] targetTypes)
{
if(currentPath == null) throw new ArgumentNullException(nameof(currentPath));
if(convertSettings == null) throw new ArgumentNullException(nameof(convertSettings));
if(!convertSettings.EnableODataTypeCast || targetTypes == null || !targetTypes.Any()) return;
foreach(var targetType in targetTypes)
{
var castPath = currentPath.Clone();
castPath.Push(new ODataTypeCastSegment(targetType));
AppendPath(castPath);
}
}
/// <summary>
/// Retrieve all bounding <see cref="IEdmOperation"/>.
/// </summary>

View file

@ -14,7 +14,7 @@ namespace Microsoft.OpenApi.OData.Edm
{
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)

View file

@ -136,7 +136,8 @@ namespace Microsoft.OpenApi.OData.Generator
if (parameterNameMapping != null)
{
parameter.Description = $"Usage: {edmParameter.Name}={{{parameterNameMapping[edmParameter.Name]}}}";
var quote = edmParameter.Type.Definition.ShouldPathParameterBeQuoted() ? "'" : string.Empty;
parameter.Description = $"Usage: {edmParameter.Name}={quote}{{{parameterNameMapping[edmParameter.Name]}}}{quote}";
}
parameters.Add(parameter);
@ -203,7 +204,8 @@ namespace Microsoft.OpenApi.OData.Generator
if (keySegment.KeyMappings != null)
{
parameter.Description = parameter.Description + $", {keyProperty.Name}={{{parameter.Name}}}";
var quote = keyProperty.Type.Definition.ShouldPathParameterBeQuoted() ? "'" : string.Empty;
parameter.Description += $", {keyProperty.Name}={quote}{{{parameter.Name}}}{quote}";
}
parameter.Extensions.Add(Constants.xMsKeyType, new OpenApiString(entityType.Name));

View file

@ -188,6 +188,11 @@ namespace Microsoft.OpenApi.OData
/// </summary>
public bool EnableDollarCountPath { get; set; } = true;
/// <summary>
/// Gets/sets a value indicating whether or not to include the OData type cast segments on entity sets.
/// </summary>
public bool EnableODataTypeCast { get; set; } = true;
internal OpenApiConvertSettings Clone()
{
var newSettings = new OpenApiConvertSettings
@ -219,6 +224,7 @@ namespace Microsoft.OpenApi.OData
ShowRootPath = this.ShowRootPath,
PathProvider = this.PathProvider,
EnableDollarCountPath = this.EnableDollarCountPath,
EnableODataTypeCast = this.EnableODataTypeCast,
};
return newSettings;

View file

@ -183,7 +183,7 @@ namespace Microsoft.OpenApi.OData.Operation
if (!LastSegmentIsKeySegment && NavigationProperty.TargetMultiplicity() == EdmMultiplicity.Many)
{
// Need to verify that TopSupported or others should be applyed to navigaiton source.
// Need to verify that TopSupported or others should be applied to navigation source.
// So, how about for the navigation property.
OpenApiParameter parameter = Context.CreateTop(NavigationProperty);
if (parameter != null)

View file

@ -101,33 +101,29 @@ namespace Microsoft.OpenApi.OData.Operation
NavigationSource.Name
};
foreach (var segment in Path.Segments.Skip(1))
foreach (var segment in Path.Segments.Skip(1).OfType<ODataNavigationPropertySegment>())
{
ODataNavigationPropertySegment npSegment = segment as ODataNavigationPropertySegment;
if (npSegment != null)
if (segment.NavigationProperty == NavigationProperty)
{
if (npSegment.NavigationProperty == NavigationProperty)
items.Add(NavigationProperty.ToEntityType().Name);
break;
}
else
{
if (items.Count >= Context.Settings.TagDepth - 1)
{
items.Add(NavigationProperty.ToEntityType().Name);
items.Add(segment.NavigationProperty.ToEntityType().Name);
break;
}
else
{
if (items.Count >= Context.Settings.TagDepth - 1)
{
items.Add(npSegment.NavigationProperty.ToEntityType().Name);
break;
}
else
{
items.Add(npSegment.NavigationProperty.Name);
}
items.Add(segment.NavigationProperty.Name);
}
}
}
string name = string.Join(".", items);
OpenApiTag tag = new OpenApiTag
OpenApiTag tag = new()
{
Name = name
};
@ -155,28 +151,24 @@ namespace Microsoft.OpenApi.OData.Operation
};
var lastpath = Path.Segments.Last(c => c is ODataNavigationPropertySegment);
foreach (var segment in Path.Segments.Skip(1))
foreach (var segment in Path.Segments.Skip(1).OfType<ODataNavigationPropertySegment>())
{
ODataNavigationPropertySegment npSegment = segment as ODataNavigationPropertySegment;
if (npSegment != null)
if (segment == lastpath)
{
if (segment == lastpath)
if (prefix != null)
{
if (prefix != null)
{
items.Add(prefix + Utils.UpperFirstChar(npSegment.NavigationProperty.Name));
}
else
{
items.Add(Utils.UpperFirstChar(npSegment.NavigationProperty.Name));
}
break;
items.Add(prefix + Utils.UpperFirstChar(segment.NavigationProperty.Name));
}
else
{
items.Add(npSegment.NavigationProperty.Name);
items.Add(Utils.UpperFirstChar(segment.NavigationProperty.Name));
}
break;
}
else
{
items.Add(segment.NavigationProperty.Name);
}
}

View file

@ -0,0 +1,410 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// ------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Vocabularies;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.OData.Common;
using Microsoft.OpenApi.OData.Edm;
using Microsoft.OpenApi.OData.Generator;
using Microsoft.OpenApi.OData.Vocabulary.Capabilities;
namespace Microsoft.OpenApi.OData.Operation;
/// <summary>
/// Retrieves a .../namespace.typename get
/// </summary>
internal class ODataTypeCastGetOperationHandler : OperationHandler
{
/// <inheritdoc/>
public override OperationType OperationType => OperationType.Get;
/// <summary>
/// Gets/sets the segment before cast.
/// this segment could be "entity set", "Collection property", etc.
/// </summary>
internal ODataSegment LastSecondSegment { get; set; }
private bool isKeySegment;
private bool IsSingleElement
{
get => isKeySegment ||
singleton != null ||
(navigationProperty != null &&
!navigationProperty.Type.IsCollection() &&
entitySet == null);
}
private NavigationPropertyRestriction restriction;
private IEdmSingleton singleton;
private IEdmEntitySet entitySet;
private IEdmNavigationProperty navigationProperty;
private IEdmEntityType parentEntityType;
private IEdmEntityType targetEntityType;
private const int SecondLastSegmentIndex = 2;
/// <inheritdoc/>
protected override void Initialize(ODataContext context, ODataPath path)
{
base.Initialize(context, path);
// get the last second segment
int count = path.Segments.Count;
if(count >= SecondLastSegmentIndex)
LastSecondSegment = path.Segments.ElementAt(count - SecondLastSegmentIndex);
parentEntityType = LastSecondSegment.EntityType;
if(LastSecondSegment is ODataNavigationPropertySegment navigationPropertySegment)
{
SetNavigationPropertyAndRestrictionFromNavigationSegment(navigationPropertySegment, path);
}
else if(LastSecondSegment is ODataNavigationSourceSegment sourceSegment)
{
if(sourceSegment.NavigationSource is IEdmEntitySet)
SetEntitySetAndRestrictionFromSourceSegment(sourceSegment);
else if (sourceSegment.NavigationSource is IEdmSingleton)
SetSingletonAndRestrictionFromSourceSegment(sourceSegment);
}
else if(LastSecondSegment is ODataKeySegment)
{
isKeySegment = true;
var thirdLastSegment = path.Segments.ElementAt(count - SecondLastSegmentIndex - 1);
if(thirdLastSegment is ODataNavigationPropertySegment navigationPropertySegment1)
{
SetNavigationPropertyAndRestrictionFromNavigationSegment(navigationPropertySegment1, path);
}
else if(thirdLastSegment is ODataNavigationSourceSegment sourceSegment1)
{
SetEntitySetAndRestrictionFromSourceSegment(sourceSegment1);
}
}
if(path.Last() is ODataTypeCastSegment oDataTypeCastSegment)
{
targetEntityType = oDataTypeCastSegment.EntityType;
}
else throw new NotImplementedException($"type cast type {path.Last().GetType().FullName} not implemented");
}
private void SetNavigationPropertyAndRestrictionFromNavigationSegment(ODataNavigationPropertySegment navigationPropertySegment, ODataPath path)
{
navigationProperty = navigationPropertySegment.NavigationProperty;
var navigationPropertyPath = string.Join("/",
Path.Segments.Where(s => !(s is ODataKeySegment || s is ODataNavigationSourceSegment
|| s is ODataStreamContentSegment || s is ODataStreamPropertySegment)).Select(e => e.Identifier));
if(path.FirstSegment is ODataNavigationSourceSegment navigationSourceSegment)
{
NavigationRestrictionsType navigation = navigationSourceSegment.NavigationSource switch {
IEdmEntitySet entitySet => Context.Model.GetRecord<NavigationRestrictionsType>(entitySet, CapabilitiesConstants.NavigationRestrictions),
IEdmSingleton singleton => Context.Model.GetRecord<NavigationRestrictionsType>(singleton, CapabilitiesConstants.NavigationRestrictions),
_ => null
};
if (navigation?.RestrictedProperties != null)
{
restriction = navigation.RestrictedProperties.FirstOrDefault(r => r.NavigationProperty != null && r.NavigationProperty == navigationPropertyPath);
}
}
}
private void SetEntitySetAndRestrictionFromSourceSegment(ODataNavigationSourceSegment sourceSegment)
{
if(sourceSegment.NavigationSource is IEdmEntitySet eSet)
{
entitySet = eSet;
SetRestrictionFromAnnotable(eSet);
}
}
private void SetSingletonAndRestrictionFromSourceSegment(ODataNavigationSourceSegment sourceSegment)
{
if(sourceSegment.NavigationSource is IEdmSingleton sTon)
{
singleton = sTon;
SetRestrictionFromAnnotable(sTon);
}
}
private void SetRestrictionFromAnnotable(IEdmVocabularyAnnotatable annotable)
{
NavigationRestrictionsType navigation = Context.Model.GetRecord<NavigationRestrictionsType>(annotable, CapabilitiesConstants.NavigationRestrictions);
if (navigation?.RestrictedProperties != null)
{
restriction = navigation.RestrictedProperties.FirstOrDefault(r => r.NavigationProperty == null);
}
}
/// <inheritdoc/>
protected override void SetBasicInfo(OpenApiOperation operation)
{
// Summary
if(IsSingleElement)
operation.Summary = $"Get the item of type {parentEntityType.ShortQualifiedName()} as {targetEntityType.ShortQualifiedName()}";
else
operation.Summary = $"Get the items of type {targetEntityType.ShortQualifiedName()} in the {parentEntityType.ShortQualifiedName()} collection";
// OperationId
if (Context.Settings.EnableOperationId)
{
operation.OperationId = $"Get.{parentEntityType.ShortQualifiedName()}.As.{targetEntityType.ShortQualifiedName()}";
}
base.SetBasicInfo(operation);
}
/// <inheritdoc/>
protected override void SetResponses(OpenApiOperation operation)
{
if(IsSingleElement)
SetSingleResponse(operation);
else
SetCollectionResponse(operation);
operation.Responses.Add(Constants.StatusCodeDefault, Constants.StatusCodeDefault.GetResponse());
base.SetResponses(operation);
}
private void SetCollectionResponse(OpenApiOperation operation)
{
OpenApiSchema schema = null;
if (Context.Settings.EnableDerivedTypesReferencesForResponses)
{
schema = EdmModelHelper.GetDerivedTypesReferenceSchema(parentEntityType, Context.Model);
}
if (schema == null)
{
schema = new OpenApiSchema
{
Reference = new OpenApiReference
{
Type = ReferenceType.Schema,
Id = $"{parentEntityType.FullName()}.To.{targetEntityType.FullName()}"
}
};
}
var properties = new Dictionary<string, OpenApiSchema>
{
{
"value",
new OpenApiSchema
{
Type = "array",
Items = schema
}
}
};
if (Context.Settings.EnablePagination)
{
properties.Add(
"@odata.nextLink",
new OpenApiSchema
{
Type = "string"
});
}
operation.Responses = new OpenApiResponses
{
{
Constants.StatusCode200,
new OpenApiResponse
{
Description = "Retrieved entities",
Content = new Dictionary<string, OpenApiMediaType>
{
{
Constants.ApplicationJsonMediaType,
new OpenApiMediaType
{
Schema = new OpenApiSchema
{
Title = $"Collection of items of type {targetEntityType.ShortQualifiedName()} in the {parentEntityType.ShortQualifiedName()} collection",
Type = "object",
Properties = properties
}
}
}
}
}
}
};
}
private void SetSingleResponse(OpenApiOperation operation)
{
OpenApiSchema schema = null;
if (Context.Settings.EnableDerivedTypesReferencesForResponses)
{
schema = EdmModelHelper.GetDerivedTypesReferenceSchema(targetEntityType, Context.Model);
}
if (schema == null)
{
schema = new OpenApiSchema
{
Reference = new OpenApiReference
{
Type = ReferenceType.Schema,
Id = targetEntityType.FullName()
}
};
}
operation.Responses = new OpenApiResponses
{
{
Constants.StatusCode200,
new OpenApiResponse
{
Description = "Result entities",
Content = new Dictionary<string, OpenApiMediaType>
{
{
Constants.ApplicationJsonMediaType,
new OpenApiMediaType
{
Schema = schema
}
}
},
}
}
};
}
/// <inheritdoc/>
protected override void SetTags(OpenApiOperation operation)
{
IList<string> items = new List<string>
{
parentEntityType.Name,
targetEntityType.Name,
};
string name = string.Join(".", items);
OpenApiTag tag = new()
{
Name = name
};
if(!IsSingleElement)
tag.Extensions.Add(Constants.xMsTocType, new OpenApiString("page"));
operation.Tags.Add(tag);
Context.AppendTag(tag);
base.SetTags(operation);
}
/// <inheritdoc/>
protected override void SetParameters(OpenApiOperation operation)
{
base.SetParameters(operation);
if(navigationProperty != null) {
if (IsSingleElement)
{
new OpenApiParameter[] {
Context.CreateSelect(navigationProperty),
Context.CreateExpand(navigationProperty),
}
.Where(x => x != null)
.ToList()
.ForEach(p => operation.Parameters.Add(p));
}
else
{
GetParametersForAnnotableOfMany(navigationProperty)
.Union(
new OpenApiParameter[] {
Context.CreateOrderBy(navigationProperty),
Context.CreateSelect(navigationProperty),
Context.CreateExpand(navigationProperty),
})
.Where(x => x != null)
.ToList()
.ForEach(p => operation.Parameters.Add(p));
}
}
else if(entitySet != null)
{
if(IsSingleElement)
{
new OpenApiParameter[] {
Context.CreateSelect(entitySet),
Context.CreateExpand(entitySet),
}
.Where(x => x != null)
.ToList()
.ForEach(p => operation.Parameters.Add(p));
}
else
{
GetParametersForAnnotableOfMany(entitySet)
.Union(
new OpenApiParameter[] {
Context.CreateOrderBy(entitySet),
Context.CreateSelect(entitySet),
Context.CreateExpand(entitySet),
})
.Where(x => x != null)
.ToList()
.ForEach(p => operation.Parameters.Add(p));
}
}
else if(singleton != null)
{
new OpenApiParameter[] {
Context.CreateSelect(singleton),
Context.CreateExpand(singleton),
}
.Where(x => x != null)
.ToList()
.ForEach(p => operation.Parameters.Add(p));
}
}
private IEnumerable<OpenApiParameter> GetParametersForAnnotableOfMany(IEdmVocabularyAnnotatable annotable)
{
// Need to verify that TopSupported or others should be applied to navigation source.
// So, how about for the navigation property.
return new OpenApiParameter[] {
Context.CreateTop(annotable),
Context.CreateSkip(annotable),
Context.CreateSearch(annotable),
Context.CreateFilter(annotable),
Context.CreateCount(annotable),
};
}
protected override void SetSecurity(OpenApiOperation operation)
{
if (restriction == null || restriction.ReadRestrictions == null)
{
return;
}
ReadRestrictionsBase readBase = restriction.ReadRestrictions;
operation.Security = Context.CreateSecurityRequirements(readBase.Permissions).ToList();
}
protected override void SetExtensions(OpenApiOperation operation)
{
if (Context.Settings.EnablePagination && !IsSingleElement)
{
OpenApiObject extension = new()
{
{ "nextLinkName", new OpenApiString("@odata.nextLink")},
{ "operationName", new OpenApiString(Context.Settings.PageableOperationName)}
};
operation.Extensions.Add(Constants.xMsPageable, extension);
}
base.SetExtensions(operation);
}
}

View file

@ -89,6 +89,12 @@ namespace Microsoft.OpenApi.OData.Operation
{
{OperationType.Get, new DollarCountGetOperationHandler() }
}},
// .../namespace.typename (cast, get)
{ODataPathKind.TypeCast, new Dictionary<OperationType, IOperationHandler>
{
{OperationType.Get, new ODataTypeCastGetOperationHandler() },
}},
};
/// <inheritdoc/>

View file

@ -0,0 +1,24 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// ------------------------------------------------------------
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.OData.Edm;
namespace Microsoft.OpenApi.OData.PathItem;
/// <summary>
/// Path item handler for type cast for example: ~/groups/{id}/members/microsoft.graph.user
/// </summary>
internal class ODataTypeCastPathItemHandler : PathItemHandler
{
/// <inheritdoc/>
protected override ODataPathKind HandleKind => ODataPathKind.TypeCast;
/// <inheritdoc/>
protected override void SetOperations(OpenApiPathItem item)
{
AddOperation(item, OperationType.Get);
}
}

View file

@ -45,6 +45,9 @@ namespace Microsoft.OpenApi.OData.PathItem
// $count
{ ODataPathKind.DollarCount, new DollarCountPathItemHandler() },
// ~/groups/{id}/members/microsoft.graph.user
{ ODataPathKind.TypeCast, new ODataTypeCastPathItemHandler() },
// Unknown
{ ODataPathKind.Unknown, null },
};

View file

@ -3,6 +3,8 @@ abstract Microsoft.OpenApi.OData.Edm.ODataSegment.Identifier.get -> string
abstract Microsoft.OpenApi.OData.Edm.ODataSegment.Kind.get -> Microsoft.OpenApi.OData.Edm.ODataSegmentKind
Microsoft.OpenApi.OData.Common.Utils
Microsoft.OpenApi.OData.Edm.EdmModelExtensions
Microsoft.OpenApi.OData.Edm.EdmTypeExtensions
static Microsoft.OpenApi.OData.Edm.EdmTypeExtensions.ShouldPathParameterBeQuoted(this Microsoft.OData.Edm.IEdmType edmType) -> bool
Microsoft.OpenApi.OData.Edm.IODataPathProvider
Microsoft.OpenApi.OData.Edm.IODataPathProvider.CanFilter(Microsoft.OData.Edm.IEdmElement element) -> bool
Microsoft.OpenApi.OData.Edm.IODataPathProvider.GetPaths(Microsoft.OData.Edm.IEdmModel model, Microsoft.OpenApi.OData.OpenApiConvertSettings settings) -> System.Collections.Generic.IEnumerable<Microsoft.OpenApi.OData.Edm.ODataPath>
@ -59,7 +61,8 @@ Microsoft.OpenApi.OData.Edm.ODataPathKind.Operation = 3 -> Microsoft.OpenApi.ODa
Microsoft.OpenApi.OData.Edm.ODataPathKind.OperationImport = 4 -> Microsoft.OpenApi.OData.Edm.ODataPathKind
Microsoft.OpenApi.OData.Edm.ODataPathKind.Ref = 6 -> Microsoft.OpenApi.OData.Edm.ODataPathKind
Microsoft.OpenApi.OData.Edm.ODataPathKind.Singleton = 2 -> Microsoft.OpenApi.OData.Edm.ODataPathKind
Microsoft.OpenApi.OData.Edm.ODataPathKind.Unknown = 10 -> Microsoft.OpenApi.OData.Edm.ODataPathKind
Microsoft.OpenApi.OData.Edm.ODataPathKind.TypeCast = 10 -> Microsoft.OpenApi.OData.Edm.ODataPathKind
Microsoft.OpenApi.OData.Edm.ODataPathKind.Unknown = 11 -> Microsoft.OpenApi.OData.Edm.ODataPathKind
Microsoft.OpenApi.OData.Edm.ODataPathProvider
Microsoft.OpenApi.OData.Edm.ODataPathProvider.ODataPathProvider() -> void
Microsoft.OpenApi.OData.Edm.ODataRefSegment
@ -130,6 +133,8 @@ Microsoft.OpenApi.OData.OpenApiConvertSettings.PathProvider.get -> Microsoft.Ope
Microsoft.OpenApi.OData.OpenApiConvertSettings.PathProvider.set -> void
Microsoft.OpenApi.OData.OpenApiConvertSettings.EnableDollarCountPath.get -> bool
Microsoft.OpenApi.OData.OpenApiConvertSettings.EnableDollarCountPath.set -> void
Microsoft.OpenApi.OData.OpenApiConvertSettings.EnableODataTypeCast.get -> bool
Microsoft.OpenApi.OData.OpenApiConvertSettings.EnableODataTypeCast.set -> void
Microsoft.OpenApi.OData.OpenApiConvertSettings.PrefixEntityTypeNameBeforeKey.get -> bool
Microsoft.OpenApi.OData.OpenApiConvertSettings.PrefixEntityTypeNameBeforeKey.set -> void
Microsoft.OpenApi.OData.OpenApiConvertSettings.RequireDerivedTypesConstraintForBoundOperations.get -> bool

View file

@ -83,7 +83,7 @@ namespace Microsoft.OpenApi.OData.Edm.Tests
};
// Assert
Assert.Equal("firstName={firstName},lastName={lastName}", segment.GetPathItemName(settings));
Assert.Equal("firstName='{firstName}',lastName='{lastName}'", segment.GetPathItemName(settings));
}
}
}

View file

@ -80,7 +80,7 @@ namespace Microsoft.OpenApi.OData.Edm.Tests
var segment = new ODataOperationImportSegment(_functionImport);
// Assert
Assert.Equal("MyFunction(firstName={firstName},lastName={lastName})",
Assert.Equal("MyFunction(firstName='{firstName}',lastName='{lastName}')",
segment.GetPathItemName(new OpenApiConvertSettings()));
}
}

View file

@ -98,9 +98,9 @@ namespace Microsoft.OpenApi.OData.Edm.Tests
[Theory]
[InlineData(true, true, "{param}")]
[InlineData(true, false, "NS.MyFunction(param={param})")]
[InlineData(false, true, "NS.MyFunction(param={param})")]
[InlineData(false, false, "NS.MyFunction(param={param})")]
[InlineData(true, false, "NS.MyFunction(param='{param}')")]
[InlineData(false, true, "NS.MyFunction(param='{param}')")]
[InlineData(false, false, "NS.MyFunction(param='{param}')")]
public void GetPathItemNameReturnsCorrectFunctionLiteralForEscapedFunction(bool isEscapedFunction, bool enableEscapeFunctionCall, string expected)
{
// Arrange & Act
@ -121,9 +121,9 @@ namespace Microsoft.OpenApi.OData.Edm.Tests
[Theory]
[InlineData(true, true, "{param}:")]
[InlineData(true, false, "NS.MyFunction(param={param})")]
[InlineData(false, true, "NS.MyFunction(param={param})")]
[InlineData(false, false, "NS.MyFunction(param={param})")]
[InlineData(true, false, "NS.MyFunction(param='{param}')")]
[InlineData(false, true, "NS.MyFunction(param='{param}')")]
[InlineData(false, false, "NS.MyFunction(param='{param}')")]
public void GetPathItemNameReturnsCorrectFunctionLiteralForEscapedComposableFunction(bool isEscapedFunction, bool enableEscapeFunctionCall, string expected)
{
// Arrange & Act

View file

@ -249,10 +249,10 @@ namespace Microsoft.OpenApi.OData.Edm.Tests
}
[Theory]
[InlineData(true, true, "/Customers/FirstName={FirstName},LastName={LastName}")]
[InlineData(true, false, "/Customers/FirstName={FirstName},LastName={LastName}")]
[InlineData(false, true, "/Customers(FirstName={FirstName},LastName={LastName})")]
[InlineData(false, false, "/Customers(FirstName={FirstName},LastName={LastName})")]
[InlineData(true, true, "/Customers/FirstName='{FirstName}',LastName='{LastName}'")]
[InlineData(true, false, "/Customers/FirstName='{FirstName}',LastName='{LastName}'")]
[InlineData(false, true, "/Customers(FirstName='{FirstName}',LastName='{LastName}')")]
[InlineData(false, false, "/Customers(FirstName='{FirstName}',LastName='{LastName}')")]
public void GetPathItemNameReturnsCorrectStringWithMultipleKeySegment(bool keyAsSegment, bool prefix, string expected)
{
// Arrange

View file

@ -75,12 +75,12 @@ namespace Microsoft.OpenApi.OData.Generator.Tests
[InlineData(true, true, true, "/Customers({ID}):/{param}:")]
[InlineData(true, true, false, "/Customers({ID}):/{param}")]
[InlineData(true, false, true, "/Customers({ID})/NS.MyFunction(param={param})")]
[InlineData(true, false, false, "/Customers({ID})/NS.MyFunction(param={param})")]
[InlineData(false, true, true, "/Customers({ID})/NS.MyFunction(param={param})")]
[InlineData(false, true, false, "/Customers({ID})/NS.MyFunction(param={param})")]
[InlineData(false, false, true, "/Customers({ID})/NS.MyFunction(param={param})")]
[InlineData(false, false, false, "/Customers({ID})/NS.MyFunction(param={param})")]
[InlineData(true, false, true, "/Customers({ID})/NS.MyFunction(param='{param}')")]
[InlineData(true, false, false, "/Customers({ID})/NS.MyFunction(param='{param}')")]
[InlineData(false, true, true, "/Customers({ID})/NS.MyFunction(param='{param}')")]
[InlineData(false, true, false, "/Customers({ID})/NS.MyFunction(param='{param}')")]
[InlineData(false, false, true, "/Customers({ID})/NS.MyFunction(param='{param}')")]
[InlineData(false, false, false, "/Customers({ID})/NS.MyFunction(param='{param}')")]
public void CreatePathItemsReturnsForEscapeFunctionModel(bool enableEscaped, bool hasEscapedAnnotation, bool isComposable, string expected)
{
// Arrange

View file

@ -7,6 +7,7 @@
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\..\tool\35MSSharedLib1024.snk</AssemblyOriginatorKeyFile>
<DelaySign>true</DelaySign>
<LangVersion>latest</LangVersion>
<OutputPath Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">..\..\bin\debug\test\</OutputPath>
<OutputPath Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">..\..\bin\release\test\</OutputPath>
</PropertyGroup>

View file

@ -134,7 +134,7 @@ namespace Microsoft.OpenApi.OData.Operation.Tests
if (enableOperationId)
{
Assert.Equal("FunctionImport.MyFunction-3e3f", operation.OperationId);
Assert.Equal("FunctionImport.MyFunction-cc1c", operation.OperationId);
}
else
{

View file

@ -229,7 +229,7 @@ namespace Microsoft.OpenApi.OData.Operation.Tests
if (enableOperationId)
{
Assert.Equal("Customers.Customer.MyFunction-28ae", operation.OperationId);
Assert.Equal("Customers.Customer.MyFunction-df74", operation.OperationId);
}
else
{

View file

@ -0,0 +1,294 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// ------------------------------------------------------------
using System.Linq;
using Microsoft.OData.Edm;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.OData.Edm;
using Microsoft.OpenApi.OData.PathItem.Tests;
using Microsoft.OpenApi.OData.Tests;
using Xunit;
namespace Microsoft.OpenApi.OData.Operation.Tests;
public class ODataTypeCastGetOperationHandlerTests
{
private readonly ODataTypeCastGetOperationHandler _operationHandler = new ();
[Theory]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public void CreateODataTypeCastGetOperationReturnsCorrectOperationForCollectionNavigationProperty(bool enableOperationId, bool enablePagination)
{// ../People/{id}/Friends/Microsoft.OData.Service.Sample.TrippinInMemory.Models.Employee
// Arrange
IEdmModel model = EdmModelHelper.TripServiceModel;
OpenApiConvertSettings settings = new()
{
EnableOperationId = enableOperationId,
EnablePagination = enablePagination,
};
ODataContext context = new(model, settings);
IEdmEntitySet people = model.EntityContainer.FindEntitySet("People");
Assert.NotNull(people);
IEdmEntityType person = model.SchemaElements.OfType<IEdmEntityType>().First(c => c.Name == "Person");
IEdmEntityType employee = model.SchemaElements.OfType<IEdmEntityType>().First(c => c.Name == "Employee");
IEdmNavigationProperty navProperty = person.DeclaredNavigationProperties().First(c => c.Name == "Friends");
ODataPath path = new(new ODataNavigationSourceSegment(people),
new ODataKeySegment(people.EntityType()),
new ODataNavigationPropertySegment(navProperty),
new ODataTypeCastSegment(employee));
// Act
var operation = _operationHandler.CreateOperation(context, path);
// Assert
Assert.NotNull(operation);
Assert.Equal("Get the items of type Microsoft.OData.Service.Sample.TrippinInMemory.Models.Employee in the Microsoft.OData.Service.Sample.TrippinInMemory.Models.Person collection", operation.Summary);
Assert.NotNull(operation.Tags);
var tag = Assert.Single(operation.Tags);
Assert.Equal("Person.Employee", tag.Name);
Assert.Single(tag.Extensions);
Assert.NotNull(operation.Parameters);
Assert.Equal(9, operation.Parameters.Count);
Assert.Null(operation.RequestBody);
if(enablePagination)
Assert.Single(operation.Extensions);
Assert.Equal(2, operation.Responses.Count);
Assert.Equal(new string[] { "200", "default" }, operation.Responses.Select(e => e.Key));
if (enableOperationId)
{
Assert.Equal("Get.Microsoft.OData.Service.Sample.TrippinInMemory.Models.Person.As.Microsoft.OData.Service.Sample.TrippinInMemory.Models.Employee", operation.OperationId);
}
else
{
Assert.Null(operation.OperationId);
}
Assert.True(operation.Responses["200"].Content["application/json"].Schema.Properties.ContainsKey("value"));
}
[Theory]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public void CreateODataTypeCastGetOperationReturnsCorrectOperationForEntitySet(bool enableOperationId, bool enablePagination)
{// .../People/Microsoft.OData.Service.Sample.TrippinInMemory.Models.Employee
// Arrange
IEdmModel model = EdmModelHelper.TripServiceModel;
OpenApiConvertSettings settings = new()
{
EnableOperationId = enableOperationId,
EnablePagination = enablePagination,
};
ODataContext context = new(model, settings);
IEdmEntitySet people = model.EntityContainer.FindEntitySet("People");
Assert.NotNull(people);
IEdmEntityType employee = model.SchemaElements.OfType<IEdmEntityType>().First(c => c.Name == "Employee");
ODataPath path = new(new ODataNavigationSourceSegment(people),
new ODataTypeCastSegment(employee));
// Act
var operation = _operationHandler.CreateOperation(context, path);
// Assert
Assert.NotNull(operation);
Assert.Equal("Get the items of type Microsoft.OData.Service.Sample.TrippinInMemory.Models.Employee in the Microsoft.OData.Service.Sample.TrippinInMemory.Models.Person collection", operation.Summary);
Assert.NotNull(operation.Tags);
var tag = Assert.Single(operation.Tags);
Assert.Equal("Person.Employee", tag.Name);
Assert.Single(tag.Extensions);
Assert.NotNull(operation.Parameters);
Assert.Equal(8, operation.Parameters.Count);
Assert.Null(operation.RequestBody);
if(enablePagination)
Assert.Single(operation.Extensions);
Assert.Equal(2, operation.Responses.Count);
Assert.Equal(new string[] { "200", "default" }, operation.Responses.Select(e => e.Key));
if (enableOperationId)
{
Assert.Equal("Get.Microsoft.OData.Service.Sample.TrippinInMemory.Models.Person.As.Microsoft.OData.Service.Sample.TrippinInMemory.Models.Employee", operation.OperationId);
}
else
{
Assert.Null(operation.OperationId);
}
Assert.True(operation.Responses["200"].Content["application/json"].Schema.Properties.ContainsKey("value"));
}
[Theory]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public void CreateODataTypeCastGetOperationReturnsCorrectOperationForEntitySetId(bool enableOperationId, bool enablePagination)
{// .../People/{id}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.Employee
// Arrange
IEdmModel model = EdmModelHelper.TripServiceModel;
OpenApiConvertSettings settings = new()
{
EnableOperationId = enableOperationId,
EnablePagination = enablePagination,
};
ODataContext context = new(model, settings);
IEdmEntitySet people = model.EntityContainer.FindEntitySet("People");
Assert.NotNull(people);
IEdmEntityType person = model.SchemaElements.OfType<IEdmEntityType>().First(c => c.Name == "Person");
IEdmEntityType employee = model.SchemaElements.OfType<IEdmEntityType>().First(c => c.Name == "Employee");
ODataPath path = new(new ODataNavigationSourceSegment(people),
new ODataKeySegment(people.EntityType()),
new ODataTypeCastSegment(employee));
// Act
var operation = _operationHandler.CreateOperation(context, path);
// Assert
Assert.NotNull(operation);
Assert.Equal("Get the item of type Microsoft.OData.Service.Sample.TrippinInMemory.Models.Person as Microsoft.OData.Service.Sample.TrippinInMemory.Models.Employee", operation.Summary);
Assert.NotNull(operation.Tags);
var tag = Assert.Single(operation.Tags);
Assert.Equal("Person.Employee", tag.Name);
Assert.Empty(tag.Extensions);
Assert.NotNull(operation.Parameters);
Assert.Equal(3, operation.Parameters.Count); //select, expand, id
Assert.Null(operation.RequestBody);
if(enablePagination)
Assert.Empty(operation.Extensions);
Assert.Equal(2, operation.Responses.Count);
Assert.Equal(new string[] { "200", "default" }, operation.Responses.Select(e => e.Key));
if (enableOperationId)
{
Assert.Equal("Get.Microsoft.OData.Service.Sample.TrippinInMemory.Models.Person.As.Microsoft.OData.Service.Sample.TrippinInMemory.Models.Employee", operation.OperationId);
}
else
{
Assert.Null(operation.OperationId);
}
Assert.False(operation.Responses["200"].Content["application/json"].Schema.Properties.ContainsKey("value"));
}
[Theory]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public void CreateODataTypeCastGetOperationReturnsCorrectOperationForSingleNavigationproperty(bool enableOperationId, bool enablePagination)
{// .../People/{id}/BestFriend/Microsoft.OData.Service.Sample.TrippinInMemory.Models.Employee
// Arrange
IEdmModel model = EdmModelHelper.TripServiceModel;
OpenApiConvertSettings settings = new()
{
EnableOperationId = enableOperationId,
EnablePagination = enablePagination,
};
ODataContext context = new(model, settings);
IEdmEntitySet people = model.EntityContainer.FindEntitySet("People");
Assert.NotNull(people);
IEdmEntityType person = model.SchemaElements.OfType<IEdmEntityType>().First(c => c.Name == "Person");
IEdmEntityType employee = model.SchemaElements.OfType<IEdmEntityType>().First(c => c.Name == "Employee");
IEdmNavigationProperty navProperty = person.DeclaredNavigationProperties().First(c => c.Name == "BestFriend");
ODataPath path = new(new ODataNavigationSourceSegment(people),
new ODataKeySegment(people.EntityType()),
new ODataNavigationPropertySegment(navProperty),
new ODataTypeCastSegment(employee));
// Act
var operation = _operationHandler.CreateOperation(context, path);
// Assert
Assert.NotNull(operation);
Assert.Equal("Get the item of type Microsoft.OData.Service.Sample.TrippinInMemory.Models.Person as Microsoft.OData.Service.Sample.TrippinInMemory.Models.Employee", operation.Summary);
Assert.NotNull(operation.Tags);
var tag = Assert.Single(operation.Tags);
Assert.Equal("Person.Employee", tag.Name);
Assert.Empty(tag.Extensions);
Assert.NotNull(operation.Parameters);
Assert.Equal(3, operation.Parameters.Count); //select, expand, id
Assert.Null(operation.RequestBody);
if(enablePagination)
Assert.Empty(operation.Extensions);
Assert.Equal(2, operation.Responses.Count);
Assert.Equal(new string[] { "200", "default" }, operation.Responses.Select(e => e.Key));
if (enableOperationId)
{
Assert.Equal("Get.Microsoft.OData.Service.Sample.TrippinInMemory.Models.Person.As.Microsoft.OData.Service.Sample.TrippinInMemory.Models.Employee", operation.OperationId);
}
else
{
Assert.Null(operation.OperationId);
}
Assert.False(operation.Responses["200"].Content["application/json"].Schema.Properties.ContainsKey("value"));
}
[Theory]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public void CreateODataTypeCastGetOperationReturnsCorrectOperationForSingleton(bool enableOperationId, bool enablePagination)
{// .../Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.Employee
// Arrange
IEdmModel model = EdmModelHelper.TripServiceModel;
OpenApiConvertSettings settings = new()
{
EnableOperationId = enableOperationId,
EnablePagination = enablePagination,
};
ODataContext context = new(model, settings);
IEdmSingleton me = model.EntityContainer.FindSingleton("Me");
Assert.NotNull(me);
IEdmEntityType employee = model.SchemaElements.OfType<IEdmEntityType>().First(c => c.Name == "Employee");
ODataPath path = new(new ODataNavigationSourceSegment(me),
new ODataTypeCastSegment(employee));
// Act
var operation = _operationHandler.CreateOperation(context, path);
// Assert
Assert.NotNull(operation);
Assert.Equal("Get the item of type Microsoft.OData.Service.Sample.TrippinInMemory.Models.Person as Microsoft.OData.Service.Sample.TrippinInMemory.Models.Employee", operation.Summary);
Assert.NotNull(operation.Tags);
var tag = Assert.Single(operation.Tags);
Assert.Equal("Person.Employee", tag.Name);
Assert.Empty(tag.Extensions);
Assert.NotNull(operation.Parameters);
Assert.Equal(2, operation.Parameters.Count); //select, expand
Assert.Null(operation.RequestBody);
if(enablePagination)
Assert.Empty(operation.Extensions);
Assert.Equal(2, operation.Responses.Count);
Assert.Equal(new string[] { "200", "default" }, operation.Responses.Select(e => e.Key));
if (enableOperationId)
{
Assert.Equal("Get.Microsoft.OData.Service.Sample.TrippinInMemory.Models.Person.As.Microsoft.OData.Service.Sample.TrippinInMemory.Models.Employee", operation.OperationId);
}
else
{
Assert.Null(operation.OperationId);
}
Assert.False(operation.Responses["200"].Content["application/json"].Schema.Properties.ContainsKey("value"));
}
}

View file

@ -22,6 +22,7 @@ namespace Microsoft.OpenApi.OData.PathItem.Tests
[InlineData(ODataPathKind.MediaEntity, typeof(MediaEntityPathItemHandler))]
[InlineData(ODataPathKind.Metadata, typeof(MetadataPathItemHandler))]
[InlineData(ODataPathKind.DollarCount, typeof(DollarCountPathItemHandler))]
[InlineData(ODataPathKind.TypeCast, typeof(ODataTypeCastPathItemHandler))]
public void GetHandlerReturnsCorrectHandlerType(ODataPathKind pathKind, Type handlerType)
{
// Arrange

View file

@ -1,4 +1,4 @@
{
{
"swagger": "2.0",
"info": {
"title": "OData Service for namespace Microsoft.OData.Service.Sample.TrippinInMemory.Models",
@ -1206,7 +1206,7 @@
"x-ms-docs-operation-type": "function"
}
},
"/Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName={userName})": {
"/Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName='{userName}')": {
"get": {
"tags": [
"Me.Functions"
@ -1220,7 +1220,7 @@
{
"in": "path",
"name": "userName",
"description": "Usage: userName={userName}",
"description": "Usage: userName='{userName}'",
"required": true,
"type": "string"
}
@ -1350,7 +1350,7 @@
"x-ms-docs-operation-type": "action"
}
},
"/Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName={lastName})": {
"/Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName='{lastName}')": {
"get": {
"tags": [
"Me.Functions"
@ -1364,7 +1364,7 @@
{
"in": "path",
"name": "lastName",
"description": "Usage: lastName={lastName}",
"description": "Usage: lastName='{lastName}'",
"required": true,
"type": "string"
}
@ -2949,7 +2949,7 @@
"x-ms-docs-operation-type": "function"
}
},
"/NewComePeople/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName={userName})": {
"/NewComePeople/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName='{userName}')": {
"get": {
"tags": [
"NewComePeople.Functions"
@ -2971,7 +2971,7 @@
{
"in": "path",
"name": "userName",
"description": "Usage: userName={userName}",
"description": "Usage: userName='{userName}'",
"required": true,
"type": "string"
}
@ -3117,7 +3117,7 @@
"x-ms-docs-operation-type": "action"
}
},
"/NewComePeople/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName={lastName})": {
"/NewComePeople/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName='{lastName}')": {
"get": {
"tags": [
"NewComePeople.Functions"
@ -3139,7 +3139,7 @@
{
"in": "path",
"name": "lastName",
"description": "Usage: lastName={lastName}",
"description": "Usage: lastName='{lastName}'",
"required": true,
"type": "string"
}
@ -4842,7 +4842,7 @@
"x-ms-docs-operation-type": "function"
}
},
"/People/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName={userName})": {
"/People/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName='{userName}')": {
"get": {
"tags": [
"People.Functions"
@ -4864,7 +4864,7 @@
{
"in": "path",
"name": "userName",
"description": "Usage: userName={userName}",
"description": "Usage: userName='{userName}'",
"required": true,
"type": "string"
}
@ -5010,7 +5010,7 @@
"x-ms-docs-operation-type": "action"
}
},
"/People/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName={lastName})": {
"/People/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName='{lastName}')": {
"get": {
"tags": [
"People.Functions"
@ -5032,7 +5032,7 @@
{
"in": "path",
"name": "lastName",
"description": "Usage: lastName={lastName}",
"description": "Usage: lastName='{lastName}'",
"required": true,
"type": "string"
}

View file

@ -1,4 +1,4 @@
swagger: '2.0'
swagger: '2.0'
info:
title: OData Service for namespace Microsoft.OData.Service.Sample.TrippinInMemory.Models
description: This OData service is located at http://services.odata.org/TrippinRESTierService
@ -822,7 +822,7 @@ paths:
default:
$ref: '#/responses/error'
x-ms-docs-operation-type: function
'/Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName={userName})':
'/Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName=''{userName}'')':
get:
tags:
- Me.Functions
@ -833,7 +833,7 @@ paths:
parameters:
- in: path
name: userName
description: 'Usage: userName={userName}'
description: 'Usage: userName=''{userName}'''
required: true
type: string
responses:
@ -919,7 +919,7 @@ paths:
default:
$ref: '#/responses/error'
x-ms-docs-operation-type: action
'/Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName={lastName})':
'/Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName=''{lastName}'')':
get:
tags:
- Me.Functions
@ -930,7 +930,7 @@ paths:
parameters:
- in: path
name: lastName
description: 'Usage: lastName={lastName}'
description: 'Usage: lastName=''{lastName}'''
required: true
type: string
responses:
@ -2038,7 +2038,7 @@ paths:
default:
$ref: '#/responses/error'
x-ms-docs-operation-type: function
'/NewComePeople/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName={userName})':
'/NewComePeople/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName=''{userName}'')':
get:
tags:
- NewComePeople.Functions
@ -2055,7 +2055,7 @@ paths:
x-ms-docs-key-type: Person
- in: path
name: userName
description: 'Usage: userName={userName}'
description: 'Usage: userName=''{userName}'''
required: true
type: string
responses:
@ -2153,7 +2153,7 @@ paths:
default:
$ref: '#/responses/error'
x-ms-docs-operation-type: action
'/NewComePeople/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName={lastName})':
'/NewComePeople/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName=''{lastName}'')':
get:
tags:
- NewComePeople.Functions
@ -2170,7 +2170,7 @@ paths:
x-ms-docs-key-type: Person
- in: path
name: lastName
description: 'Usage: lastName={lastName}'
description: 'Usage: lastName=''{lastName}'''
required: true
type: string
responses:
@ -3364,7 +3364,7 @@ paths:
default:
$ref: '#/responses/error'
x-ms-docs-operation-type: function
'/People/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName={userName})':
'/People/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName=''{userName}'')':
get:
tags:
- People.Functions
@ -3381,7 +3381,7 @@ paths:
x-ms-docs-key-type: Person
- in: path
name: userName
description: 'Usage: userName={userName}'
description: 'Usage: userName=''{userName}'''
required: true
type: string
responses:
@ -3479,7 +3479,7 @@ paths:
default:
$ref: '#/responses/error'
x-ms-docs-operation-type: action
'/People/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName={lastName})':
'/People/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName=''{lastName}'')':
get:
tags:
- People.Functions
@ -3496,7 +3496,7 @@ paths:
x-ms-docs-key-type: Person
- in: path
name: lastName
description: 'Usage: lastName={lastName}'
description: 'Usage: lastName=''{lastName}'''
required: true
type: string
responses:

View file

@ -1,4 +1,4 @@
{
{
"openapi": "3.0.1",
"info": {
"title": "OData Service for namespace Microsoft.OData.Service.Sample.TrippinInMemory.Models",
@ -1370,7 +1370,7 @@
"x-ms-docs-operation-type": "function"
}
},
"/Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName={userName})": {
"/Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName='{userName}')": {
"get": {
"tags": [
"Me.Functions"
@ -1381,7 +1381,7 @@
{
"name": "userName",
"in": "path",
"description": "Usage: userName={userName}",
"description": "Usage: userName='{userName}'",
"required": true,
"schema": {
"type": "string"
@ -1522,7 +1522,7 @@
"x-ms-docs-operation-type": "action"
}
},
"/Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName={lastName})": {
"/Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName='{lastName}')": {
"get": {
"tags": [
"Me.Functions"
@ -1533,7 +1533,7 @@
{
"name": "lastName",
"in": "path",
"description": "Usage: lastName={lastName}",
"description": "Usage: lastName='{lastName}'",
"required": true,
"schema": {
"type": "string"
@ -3304,7 +3304,7 @@
"x-ms-docs-operation-type": "function"
}
},
"/NewComePeople/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName={userName})": {
"/NewComePeople/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName='{userName}')": {
"get": {
"tags": [
"NewComePeople.Functions"
@ -3325,7 +3325,7 @@
{
"name": "userName",
"in": "path",
"description": "Usage: userName={userName}",
"description": "Usage: userName='{userName}'",
"required": true,
"schema": {
"type": "string"
@ -3490,7 +3490,7 @@
"x-ms-docs-operation-type": "action"
}
},
"/NewComePeople/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName={lastName})": {
"/NewComePeople/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName='{lastName}')": {
"get": {
"tags": [
"NewComePeople.Functions"
@ -3511,7 +3511,7 @@
{
"name": "lastName",
"in": "path",
"description": "Usage: lastName={lastName}",
"description": "Usage: lastName='{lastName}'",
"required": true,
"schema": {
"type": "string"
@ -5427,7 +5427,7 @@
"x-ms-docs-operation-type": "function"
}
},
"/People/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName={userName})": {
"/People/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName='{userName}')": {
"get": {
"tags": [
"People.Functions"
@ -5448,7 +5448,7 @@
{
"name": "userName",
"in": "path",
"description": "Usage: userName={userName}",
"description": "Usage: userName='{userName}'",
"required": true,
"schema": {
"type": "string"
@ -5613,7 +5613,7 @@
"x-ms-docs-operation-type": "action"
}
},
"/People/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName={lastName})": {
"/People/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName='{lastName}')": {
"get": {
"tags": [
"People.Functions"
@ -5634,7 +5634,7 @@
{
"name": "lastName",
"in": "path",
"description": "Usage: lastName={lastName}",
"description": "Usage: lastName='{lastName}'",
"required": true,
"schema": {
"type": "string"

View file

@ -1,4 +1,4 @@
openapi: 3.0.1
openapi: 3.0.1
info:
title: OData Service for namespace Microsoft.OData.Service.Sample.TrippinInMemory.Models
description: This OData service is located at http://services.odata.org/TrippinRESTierService
@ -914,7 +914,7 @@ paths:
default:
$ref: '#/components/responses/error'
x-ms-docs-operation-type: function
'/Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName={userName})':
'/Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName=''{userName}'')':
get:
tags:
- Me.Functions
@ -923,7 +923,7 @@ paths:
parameters:
- name: userName
in: path
description: 'Usage: userName={userName}'
description: 'Usage: userName=''{userName}'''
required: true
schema:
type: string
@ -1012,7 +1012,7 @@ paths:
default:
$ref: '#/components/responses/error'
x-ms-docs-operation-type: action
'/Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName={lastName})':
'/Me/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName=''{lastName}'')':
get:
tags:
- Me.Functions
@ -1021,7 +1021,7 @@ paths:
parameters:
- name: lastName
in: path
description: 'Usage: lastName={lastName}'
description: 'Usage: lastName=''{lastName}'''
required: true
schema:
type: string
@ -2238,7 +2238,7 @@ paths:
default:
$ref: '#/components/responses/error'
x-ms-docs-operation-type: function
'/NewComePeople/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName={userName})':
'/NewComePeople/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName=''{userName}'')':
get:
tags:
- NewComePeople.Functions
@ -2254,7 +2254,7 @@ paths:
x-ms-docs-key-type: Person
- name: userName
in: path
description: 'Usage: userName={userName}'
description: 'Usage: userName=''{userName}'''
required: true
schema:
type: string
@ -2359,7 +2359,7 @@ paths:
default:
$ref: '#/components/responses/error'
x-ms-docs-operation-type: action
'/NewComePeople/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName={lastName})':
'/NewComePeople/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName=''{lastName}'')':
get:
tags:
- NewComePeople.Functions
@ -2375,7 +2375,7 @@ paths:
x-ms-docs-key-type: Person
- name: lastName
in: path
description: 'Usage: lastName={lastName}'
description: 'Usage: lastName=''{lastName}'''
required: true
schema:
type: string
@ -3691,7 +3691,7 @@ paths:
default:
$ref: '#/components/responses/error'
x-ms-docs-operation-type: function
'/People/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName={userName})':
'/People/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName=''{userName}'')':
get:
tags:
- People.Functions
@ -3707,7 +3707,7 @@ paths:
x-ms-docs-key-type: Person
- name: userName
in: path
description: 'Usage: userName={userName}'
description: 'Usage: userName=''{userName}'''
required: true
schema:
type: string
@ -3812,7 +3812,7 @@ paths:
default:
$ref: '#/components/responses/error'
x-ms-docs-operation-type: action
'/People/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName={lastName})':
'/People/{UserName}/Microsoft.OData.Service.Sample.TrippinInMemory.Models.UpdatePersonLastName(lastName=''{lastName}'')':
get:
tags:
- People.Functions
@ -3828,7 +3828,7 @@ paths:
x-ms-docs-key-type: Person
- name: lastName
in: path
description: 'Usage: lastName={lastName}'
description: 'Usage: lastName=''{lastName}'''
required: true
schema:
type: string