diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/EdmModelExtensions.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/EdmModelExtensions.cs index 2accb7e..218dc8f 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/EdmModelExtensions.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/EdmModelExtensions.cs @@ -18,6 +18,25 @@ namespace Microsoft.OpenApi.OData.Edm /// public static class EdmModelExtensions { + /// + /// Determines whether the specified operation is UrlEscape function or not. + /// + /// The Edm model. + /// The specified operation. + /// true if the specified operation is UrlEscape function; otherwise, false. + public static bool IsUrlEscapeFunction(this IEdmModel model, IEdmOperation operation) + { + Utils.CheckArgumentNull(model, nameof(model)); + Utils.CheckArgumentNull(operation, nameof(operation)); + + if (operation.IsAction()) + { + return false; + } + + return model.IsUrlEscapeFunction((IEdmFunction)operation); + } + /// /// Determines whether the specified function is UrlEscape function or not. /// diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataOperationSegment.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataOperationSegment.cs index fb4d336..3a2a8d1 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataOperationSegment.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataOperationSegment.cs @@ -22,8 +22,18 @@ namespace Microsoft.OpenApi.OData.Edm /// /// The operation. public ODataOperationSegment(IEdmOperation operation) + : this(operation, false) + { + } + + /// + /// Initializes a new instance of class. + /// + /// The operation. + public ODataOperationSegment(IEdmOperation operation, bool isEscapedFunction) { Operation = operation ?? throw Error.ArgumentNull(nameof(operation)); + IsEscapedFunction = isEscapedFunction; } /// @@ -31,6 +41,11 @@ namespace Microsoft.OpenApi.OData.Edm /// public IEdmOperation Operation { get; } + /// + /// Gets the is escaped function. + /// + public bool IsEscapedFunction { get; } + /// public override ODataSegmentKind Kind => ODataSegmentKind.Operation; @@ -52,6 +67,22 @@ namespace Microsoft.OpenApi.OData.Edm private string FunctionName(IEdmFunction function, OpenApiConvertSettings settings, HashSet parameters) { + if (settings.EnableUriEscapeFunctionCall && IsEscapedFunction) + { + // Debug.Assert(function.Parameters.Count == 2); It should be verify at Edm model. + // Debug.Assert(function.IsBound == true); + string parameterName = function.Parameters.Last().Name; + parameterName = Utils.GetUniqueName(parameterName, parameters); + if (function.IsComposable) + { + return $"{{{parameterName}}}:"; + } + else + { + return $"{{{parameterName}}}"; + } + } + StringBuilder functionName = new StringBuilder(); if (settings.EnableUnqualifiedCall) { diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs index 954cde6..e99c25a 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs @@ -159,6 +159,17 @@ namespace Microsoft.OpenApi.OData.Edm } else // other segments { + if (segment.Kind == ODataSegmentKind.Operation) + { + ODataOperationSegment operation = (ODataOperationSegment)segment; + if (operation.IsEscapedFunction && settings.EnableUriEscapeFunctionCall) + { + sb.Append(":/"); + sb.Append(pathItemName); + continue; + } + } + sb.Append("/"); sb.Append(pathItemName); } diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs index b9afb22..19986be 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs @@ -319,15 +319,18 @@ namespace Microsoft.OpenApi.OData.Edm private bool AppendBoundOperationOnNavigationSourcePath(IEdmOperation edmOperation, bool isCollection, IEdmEntityType bindingEntityType) { bool found = false; + if (_allNavigationSourcePaths.TryGetValue(bindingEntityType, out IList value)) { + bool isEscapedFunction = _model.IsUrlEscapeFunction(edmOperation); + foreach (var subPath in value) { if ((isCollection && subPath.Kind == ODataPathKind.EntitySet) || (!isCollection && subPath.Kind != ODataPathKind.EntitySet)) { ODataPath newPath = subPath.Clone(); - newPath.Push(new ODataOperationSegment(edmOperation)); + newPath.Push(new ODataOperationSegment(edmOperation, isEscapedFunction)); AppendPath(newPath); found = true; } @@ -340,6 +343,7 @@ namespace Microsoft.OpenApi.OData.Edm private bool AppendBoundOperationOnNavigationPropertyPath(IEdmOperation edmOperation, bool isCollection, IEdmEntityType bindingEntityType) { bool found = false; + bool isEscapedFunction = _model.IsUrlEscapeFunction(edmOperation); if (_allNavigationPropertyPaths.TryGetValue(bindingEntityType, out IList value)) { @@ -370,7 +374,7 @@ namespace Microsoft.OpenApi.OData.Edm } ODataPath newPath = path.Clone(); - newPath.Push(new ODataOperationSegment(edmOperation)); + newPath.Push(new ODataOperationSegment(edmOperation, isEscapedFunction)); AppendPath(newPath); found = true; } @@ -383,6 +387,7 @@ namespace Microsoft.OpenApi.OData.Edm { bool found = false; + bool isEscapedFunction = _model.IsUrlEscapeFunction(edmOperation); foreach (var baseType in bindingEntityType.FindAllBaseTypes()) { if (_allNavigationSources.TryGetValue(baseType, out IList baseNavigationSource)) @@ -394,7 +399,7 @@ namespace Microsoft.OpenApi.OData.Edm if (ns is IEdmEntitySet) { ODataPath newPath = new ODataPath(new ODataNavigationSourceSegment(ns), new ODataTypeCastSegment(bindingEntityType), - new ODataOperationSegment(edmOperation)); + new ODataOperationSegment(edmOperation, isEscapedFunction)); AppendPath(newPath); found = true; } @@ -404,7 +409,7 @@ namespace Microsoft.OpenApi.OData.Edm if (ns is IEdmSingleton) { ODataPath newPath = new ODataPath(new ODataNavigationSourceSegment(ns), new ODataTypeCastSegment(bindingEntityType), - new ODataOperationSegment(edmOperation)); + new ODataOperationSegment(edmOperation, isEscapedFunction)); AppendPath(newPath); found = true; } @@ -412,7 +417,7 @@ namespace Microsoft.OpenApi.OData.Edm { ODataPath newPath = new ODataPath(new ODataNavigationSourceSegment(ns), new ODataKeySegment(ns.EntityType()), new ODataTypeCastSegment(bindingEntityType), - new ODataOperationSegment(edmOperation)); + new ODataOperationSegment(edmOperation, isEscapedFunction)); AppendPath(newPath); found = true; } diff --git a/src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs b/src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs index 9842e5b..a5c27f5 100644 --- a/src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs +++ b/src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs @@ -68,6 +68,11 @@ namespace Microsoft.OpenApi.OData /// public bool EnableOperationId { get; set; } = true; + /// + /// Gets/sets a value indicating whether to output the binding function as Uri escape function if applied the UriEscapeFunction term. + /// + public bool EnableUriEscapeFunctionCall { get; set; } = false; + /// /// Gets/sets a value indicating whether to verify the edm model before converter. /// @@ -103,6 +108,7 @@ namespace Microsoft.OpenApi.OData newSettings.VerifyEdmModel = this.VerifyEdmModel; newSettings.IEEE754Compatible = this.IEEE754Compatible; newSettings.TopExample = this.TopExample; + newSettings.EnableUriEscapeFunctionCall = this.EnableUriEscapeFunctionCall; return newSettings; } diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataOperationSegmentTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataOperationSegmentTests.cs index 88a3819..198943a 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataOperationSegmentTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataOperationSegmentTests.cs @@ -4,6 +4,7 @@ // ------------------------------------------------------------ using System; +using System.Runtime.CompilerServices; using Microsoft.OData.Edm; using Xunit; @@ -95,11 +96,57 @@ namespace Microsoft.OpenApi.OData.Edm.Tests Assert.Equal(expected, segment.GetPathItemName(settings)); } - private EdmFunction BoundFunction(string funcName, bool isBound, IEdmTypeReference firstParameterType) + [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})")] + public void GetPathItemNameReturnsCorrectFunctionLiteralForEscapedFunction(bool isEscapedFunction, bool enableEscapeFunctionCall, string expected) + { + // Arrange & Act + IEdmEntityTypeReference entityTypeReference = new EdmEntityTypeReference(new EdmEntityType("NS", "Entity"), false); + IEdmTypeReference parameterType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false); + EdmFunction boundFunction = BoundFunction("MyFunction", true, entityTypeReference); + boundFunction.AddParameter("param", parameterType); + + var segment = new ODataOperationSegment(boundFunction, isEscapedFunction); + OpenApiConvertSettings settings = new OpenApiConvertSettings + { + EnableUriEscapeFunctionCall = enableEscapeFunctionCall + }; + + // Assert + Assert.Equal(expected, segment.GetPathItemName(settings)); + } + + [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})")] + public void GetPathItemNameReturnsCorrectFunctionLiteralForEscapedComposableFunction(bool isEscapedFunction, bool enableEscapeFunctionCall, string expected) + { + // Arrange & Act + IEdmEntityTypeReference entityTypeReference = new EdmEntityTypeReference(new EdmEntityType("NS", "Entity"), false); + IEdmTypeReference parameterType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false); + EdmFunction boundFunction = BoundFunction("MyFunction", true, entityTypeReference, true); + boundFunction.AddParameter("param", parameterType); + + var segment = new ODataOperationSegment(boundFunction, isEscapedFunction); + OpenApiConvertSettings settings = new OpenApiConvertSettings + { + EnableUriEscapeFunctionCall = enableEscapeFunctionCall + }; + + // Assert + Assert.Equal(expected, segment.GetPathItemName(settings)); + } + + private EdmFunction BoundFunction(string funcName, bool isBound, IEdmTypeReference firstParameterType, bool isComposable = false) { IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); EdmFunction boundFunction = new EdmFunction("NS", funcName, returnType, - isBound: isBound, entitySetPathExpression: null, isComposable: false); + isBound: isBound, entitySetPathExpression: null, isComposable: isComposable); boundFunction.AddParameter("entity", firstParameterType); return boundFunction; } diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Generator/OpenApiPathItemGeneratorTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Generator/OpenApiPathItemGeneratorTests.cs index 2f29650..b163330 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Generator/OpenApiPathItemGeneratorTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Generator/OpenApiPathItemGeneratorTests.cs @@ -5,6 +5,9 @@ using System; using Microsoft.OData.Edm; +using Microsoft.OData.Edm.Csdl; +using Microsoft.OData.Edm.Vocabularies; +using Microsoft.OData.Edm.Vocabularies.Community.V1; using Microsoft.OpenApi.OData.Edm; using Microsoft.OpenApi.OData.Tests; using Xunit; @@ -64,5 +67,58 @@ namespace Microsoft.OpenApi.OData.Generator.Tests Assert.Contains("/CountryOrRegion/{Name}", pathItems.Keys); Assert.Contains("/Me", pathItems.Keys); } + + [Theory] + [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})")] + public void CreatePathItemsReturnsForEscapeFunctionModel(bool enableEscaped, bool hasEscapedAnnotation, bool isComposable, string expected) + { + // Arrange + EdmModel model = new EdmModel(); + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + model.AddElement(customer); + EdmFunction function = new EdmFunction("NS", "MyFunction", EdmCoreModel.Instance.GetString(false), true, null, isComposable); + function.AddParameter("entity", new EdmEntityTypeReference(customer, false)); + function.AddParameter("param", EdmCoreModel.Instance.GetString(false)); + model.AddElement(function); + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + EdmEntitySet customers = new EdmEntitySet(container, "Customers", customer); + container.AddElement(customers); + model.AddElement(container); + + if (hasEscapedAnnotation) + { + IEdmBooleanConstantExpression booleanConstant = new EdmBooleanConstant(true); + IEdmTerm term = CommunityVocabularyModel.UrlEscapeFunctionTerm; + EdmVocabularyAnnotation annotation = new EdmVocabularyAnnotation(function, term, booleanConstant); + annotation.SetSerializationLocation(model, EdmVocabularyAnnotationSerializationLocation.Inline); + model.SetVocabularyAnnotation(annotation); + } + + OpenApiConvertSettings settings = new OpenApiConvertSettings + { + EnableUriEscapeFunctionCall = enableEscaped + }; + ODataContext context = new ODataContext(model, settings); + + // Act + var pathItems = context.CreatePathItems(); + + // Assert + Assert.NotNull(pathItems); + Assert.Equal(3, pathItems.Count); + + Assert.Contains("/Customers", pathItems.Keys); + Assert.Contains("/Customers({ID})", pathItems.Keys); + Assert.Contains(expected, pathItems.Keys); + } } }