From 2a24fac462485740005dbd3c04f8b7970a815685 Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Thu, 12 Dec 2024 11:37:56 -0800 Subject: [PATCH] Fixes #2911, Add APIs to regiter custom uri functions for certain model --- .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 4 + .../UriParser/Binders/FunctionCallBinder.cs | 82 ++- .../UriParser/CustomUriFunctions.cs | 174 +++++- .../CustomUriFunctionsTests.Model.cs | 558 ++++++++++++++++++ .../UriParser/CustomUriFunctionsTests.cs | 6 +- 5 files changed, 794 insertions(+), 30 deletions(-) create mode 100644 test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/CustomUriFunctionsTests.Model.cs diff --git a/src/Microsoft.OData.Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.OData.Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt index e69de29bb2..22368c6883 100644 --- a/src/Microsoft.OData.Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.OData.Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -0,0 +1,4 @@ +static Microsoft.OData.UriParser.CustomUriFunctions.AddCustomUriFunction(this Microsoft.OData.Edm.IEdmModel model, string functionName, Microsoft.OData.UriParser.FunctionSignatureWithReturnType functionSignature) -> void +static Microsoft.OData.UriParser.CustomUriFunctions.RemoveCustomUriFunction(this Microsoft.OData.Edm.IEdmModel model, string functionName) -> bool +static Microsoft.OData.UriParser.CustomUriFunctions.RemoveCustomUriFunction(this Microsoft.OData.Edm.IEdmModel model, string functionName, Microsoft.OData.UriParser.FunctionSignatureWithReturnType functionSignature) -> bool +static Microsoft.OData.UriParser.CustomUriFunctions.TryGetCustomFunction(this Microsoft.OData.Edm.IEdmModel model, string functionCallToken, out System.Collections.Generic.IList> nameSignatures, bool enableCaseInsensitive = false) -> bool \ No newline at end of file diff --git a/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs b/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs index 7c22cdaa71..07acc0207d 100644 --- a/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs +++ b/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs @@ -146,19 +146,25 @@ internal static KeyValuePair MatchSigna /// Optional flag for whether case insensitive match is enabled. /// The signatures which match the supplied function name. [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "need to use lower characters for built-in functions.")] - internal static IList> GetUriFunctionSignatures(string functionCallToken, bool enableCaseInsensitive = false) + internal static IList> GetUriFunctionSignatures(string functionCallToken, IEdmModel model = null, bool enableCaseInsensitive = false) { IList> customUriFunctionsNameSignatures = null; string builtInUriFunctionName = null; FunctionSignatureWithReturnType[] builtInUriFunctionsSignatures = null; IList> builtInUriFunctionsNameSignatures = null; - // Try to find the function in the user custom functions - bool customFound = CustomUriFunctions.TryGetCustomFunction(functionCallToken, out customUriFunctionsNameSignatures, - enableCaseInsensitive); + // Try to find the function in the user custom functions for the specified model. + IList> customUriFunctionsNameSignaturesInModel = null; + bool customInModelFound = false; + if (model != null) + { + customInModelFound = model.TryGetCustomFunction(functionCallToken, out customUriFunctionsNameSignaturesInModel, enableCaseInsensitive); + } + + // Try to find the function in the user custom functions (From global static dictionary, we will remove it in the next major release) + bool customFound = CustomUriFunctions.TryGetCustomFunction(functionCallToken, out customUriFunctionsNameSignatures, enableCaseInsensitive); - bool builtInFound = BuiltInUriFunctions.TryGetBuiltInFunction(functionCallToken, enableCaseInsensitive, out builtInUriFunctionName, - out builtInUriFunctionsSignatures); + bool builtInFound = BuiltInUriFunctions.TryGetBuiltInFunction(functionCallToken, enableCaseInsensitive, out builtInUriFunctionName, out builtInUriFunctionsSignatures); // Populate the matched names found for built-in function if (builtInFound) @@ -167,25 +173,57 @@ internal static IList> Get builtInUriFunctionsSignatures.Select(sig => new KeyValuePair(builtInUriFunctionName, sig)).ToList(); } - if (!customFound && !builtInFound) - { - // Not found in both built-in and custom. - throw new ODataException(Error.Format(SRResources.MetadataBinder_UnknownFunction, functionCallToken)); - } - - if (!customFound) + if (builtInFound) { - Debug.Assert(builtInUriFunctionsNameSignatures != null, "No Built-in functions found"); - return builtInUriFunctionsNameSignatures; + if (customInModelFound) + { + if (customFound) + { + return builtInUriFunctionsNameSignatures.Concat(customUriFunctionsNameSignaturesInModel).Concat(customUriFunctionsNameSignatures).ToArray(); + } + else + { + return builtInUriFunctionsNameSignatures.Concat(customUriFunctionsNameSignaturesInModel).ToArray(); + } + } + else + { + if (customFound) + { + return builtInUriFunctionsNameSignatures.Concat(customUriFunctionsNameSignatures).ToArray(); + } + else + { + return builtInUriFunctionsNameSignatures; + } + } } - - if (!builtInFound) + else { - Debug.Assert(customUriFunctionsNameSignatures != null, "No Custom functions found"); - return customUriFunctionsNameSignatures; + if (customInModelFound) + { + if (customFound) + { + return customUriFunctionsNameSignaturesInModel.Concat(customUriFunctionsNameSignatures).ToArray(); + } + else + { + return customUriFunctionsNameSignaturesInModel; + } + } + else + { + if (customFound) + { + return customUriFunctionsNameSignatures; + } + else + { + // Not found in both built-in and custom. + throw new ODataException(Error.Format(SRResources.MetadataBinder_UnknownFunction, functionCallToken)); + } + } } - - return builtInUriFunctionsNameSignatures.Concat(customUriFunctionsNameSignatures).ToArray(); } internal static FunctionSignatureWithReturnType[] ExtractSignatures( @@ -300,7 +338,7 @@ private QueryNode BindAsUriFunction(FunctionCallToken functionCallToken, List> nameSignatures = GetUriFunctionSignatures(functionCallToken.Name, + IList> nameSignatures = GetUriFunctionSignatures(functionCallToken.Name, state.Model, this.state.Configuration.EnableCaseInsensitiveUriFunctionIdentifier); SingleValueNode[] argumentNodeArray = ValidateArgumentsAreSingleValue(functionCallToken.Name, argumentNodes); diff --git a/src/Microsoft.OData.Core/UriParser/CustomUriFunctions.cs b/src/Microsoft.OData.Core/UriParser/CustomUriFunctions.cs index 9105307253..5cb1b2c5da 100644 --- a/src/Microsoft.OData.Core/UriParser/CustomUriFunctions.cs +++ b/src/Microsoft.OData.Core/UriParser/CustomUriFunctions.cs @@ -9,6 +9,7 @@ namespace Microsoft.OData.UriParser #region NameSpaces using System; + using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -17,6 +18,17 @@ namespace Microsoft.OData.UriParser #endregion + /// + /// Class represents the annotation for custom functions + /// + internal class CustomUriFunctionsAnnotation + { + /// + /// Dictionary of the name of the custom function and all the signatures. + /// + public ConcurrentDictionary CustomFunctions { get; } = new ConcurrentDictionary(StringComparer.Ordinal); + } + /// /// Class represents functions signatures of custom uri functions. /// @@ -36,6 +48,158 @@ private static readonly Dictionary Cu #region Public Methods + /// + /// Add a custom uri function to extend uri functions for the certain model. + /// In case the function name already exists as a custom function, the signature will be added as an another overload. + /// + /// The given Edm model. + /// The new custom function name. + /// The new custom function signature. + public static void AddCustomUriFunction(this IEdmModel model, string functionName, FunctionSignatureWithReturnType functionSignature) + { + // Parameters validation + ExceptionUtils.CheckArgumentNotNull(model, "model"); + ExceptionUtils.CheckArgumentStringNotNullOrEmpty(functionName, "functionName"); + ExceptionUtils.CheckArgumentNotNull(functionSignature, "functionSignature"); + + ValidateFunctionWithReturnType(functionSignature); + + // Check if the function does already exists in the Built-In functions + FunctionSignatureWithReturnType[] existingBuiltInFunctionOverload; + if (BuiltInUriFunctions.TryGetBuiltInFunction(functionName, out existingBuiltInFunctionOverload)) + { + // Function name exists, check if full signature exists among the overloads. + if (existingBuiltInFunctionOverload.Any(builtInFunction => + AreFunctionsSignatureEqual(functionSignature, builtInFunction))) + { + throw new ODataException(Error.Format(SRResources.CustomUriFunctions_AddCustomUriFunction_BuiltInExistsFullSignature, functionName)); + } + } + + CustomUriFunctionsAnnotation funAnnotations = model.GetOrSetCustomUriFunctionAnnotation(); + AddCustomFunction(funAnnotations.CustomFunctions, functionName, functionSignature); + } + + /// + /// Removes the specific function overload from the custom uri functions. + /// + /// The given Edm model. + /// Custom function name to remove. + /// The specific signature overload of the function to remove. + /// 'False' if custom function signature doesn't exist. 'True' if function has been removed successfully. + /// Arguments are null, or function signature return type is null. + public static bool RemoveCustomUriFunction(this IEdmModel model, string functionName, FunctionSignatureWithReturnType functionSignature) + { + ExceptionUtils.CheckArgumentNotNull(model, "model"); + ExceptionUtils.CheckArgumentStringNotNullOrEmpty(functionName, "functionName"); + ExceptionUtils.CheckArgumentNotNull(functionSignature, "functionSignature"); + + ValidateFunctionWithReturnType(functionSignature); + + CustomUriFunctionsAnnotation funAnnotations = model.GetOrSetCustomUriFunctionAnnotation(); + ConcurrentDictionary customFunctions = funAnnotations.CustomFunctions; + + FunctionSignatureWithReturnType[] existingCustomFunctionOverloads; + if (!customFunctions.TryGetValue(functionName, out existingCustomFunctionOverloads)) + { + return false; + } + + // Get all function sigature overloads without the overload which is requested to be removed + FunctionSignatureWithReturnType[] customFunctionOverloadsWithoutTheOneToRemove = + existingCustomFunctionOverloads.SkipWhile(funcOverload => AreFunctionsSignatureEqual(funcOverload, functionSignature)).ToArray(); + + // Nothing was removed - Requested overload doesn't exist + if (customFunctionOverloadsWithoutTheOneToRemove.Length == existingCustomFunctionOverloads.Length) + { + return false; + } + + // No overloads have left in this function name. Delete the function name + if (customFunctionOverloadsWithoutTheOneToRemove.Length == 0) + { + return customFunctions.Remove(functionName, out _); + } + else + { + // Requested overload has been removed. + // Update the custom functions to the overloads without that one requested to be removed + customFunctions[functionName] = customFunctionOverloadsWithoutTheOneToRemove; + return true; + } + } + + /// + /// Removes all the function overloads from the custom uri functions. + /// + /// The given Edm model. + /// The custom function name. + /// 'False' if custom function signature doesn't exist. 'True' if function has been removed successfully + /// Arguments are null, or function signature return type is null + public static bool RemoveCustomUriFunction(this IEdmModel model, string functionName) + { + ExceptionUtils.CheckArgumentNotNull(model, "model"); + ExceptionUtils.CheckArgumentStringNotNullOrEmpty(functionName, "functionName"); + + CustomUriFunctionsAnnotation funAnnotations = model.GetOrSetCustomUriFunctionAnnotation(); + return funAnnotations.CustomFunctions.Remove(functionName, out _); + } + + /// + /// Returns a list of name-signature pairs for a function name. + /// + /// The given Edm model. + /// The name of the function to look for. + /// + /// Output for the list of signature objects for matched function names, with canonical name of the function; + /// null if no matches found. + /// + /// Whether to perform case-insensitive match for function name. + /// true if the function was found, or false otherwise. + public static bool TryGetCustomFunction(this IEdmModel model, string functionCallToken, out IList> nameSignatures, + bool enableCaseInsensitive = false) + { + ExceptionUtils.CheckArgumentNotNull(model, "model"); + + nameSignatures = null; + CustomUriFunctionsAnnotation funAnnotations = model.GetAnnotationValue(model); + if (funAnnotations == null) + { + return false; + } + + IList> bufferedKeyValuePairs + = new List>(); + + foreach (KeyValuePair func in funAnnotations.CustomFunctions) + { + if (func.Key.Equals(functionCallToken, enableCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) + { + foreach (FunctionSignatureWithReturnType sig in func.Value) + { + bufferedKeyValuePairs.Add(new KeyValuePair(func.Key, sig)); + } + } + } + + // Setup the output values. + nameSignatures = bufferedKeyValuePairs.Count != 0 ? bufferedKeyValuePairs : null; + + return nameSignatures != null; + } + + private static CustomUriFunctionsAnnotation GetOrSetCustomUriFunctionAnnotation(this IEdmModel model) + { + CustomUriFunctionsAnnotation annotation = model.GetAnnotationValue(model); + if (annotation == null) + { + annotation = new CustomUriFunctionsAnnotation(); + model.SetAnnotationValue(model, annotation); + } + + return annotation; + } + /// /// Add a custom uri function to extend uri functions. /// In case the function name already exists as a custom function, the signature will be added as an another overload. @@ -71,7 +235,7 @@ public static void AddCustomUriFunction(string functionName, FunctionSignatureWi } } - AddCustomFunction(functionName, functionSignature); + AddCustomFunction(CustomFunctions, functionName, functionSignature); } } @@ -184,14 +348,14 @@ IList> bufferedKeyValuePai #region Private Methods - private static void AddCustomFunction(string customFunctionName, FunctionSignatureWithReturnType newCustomFunctionSignature) + private static void AddCustomFunction(IDictionary customFunctions, string customFunctionName, FunctionSignatureWithReturnType newCustomFunctionSignature) { FunctionSignatureWithReturnType[] existingCustomFunctionOverloads; // In case the function doesn't already exist - if (!CustomFunctions.TryGetValue(customFunctionName, out existingCustomFunctionOverloads)) + if (!customFunctions.TryGetValue(customFunctionName, out existingCustomFunctionOverloads)) { - CustomFunctions.Add(customFunctionName, new FunctionSignatureWithReturnType[] { newCustomFunctionSignature }); + customFunctions.Add(customFunctionName, new FunctionSignatureWithReturnType[] { newCustomFunctionSignature }); } else { @@ -207,7 +371,7 @@ private static void AddCustomFunction(string customFunctionName, FunctionSignatu } // Add the custom function as an overload to the same function name - CustomFunctions[customFunctionName] = + customFunctions[customFunctionName] = existingCustomFunctionOverloads.Concat(new FunctionSignatureWithReturnType[] { newCustomFunctionSignature }).ToArray(); } } diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/CustomUriFunctionsTests.Model.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/CustomUriFunctionsTests.Model.cs new file mode 100644 index 0000000000..e7f4f69846 --- /dev/null +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/CustomUriFunctionsTests.Model.cs @@ -0,0 +1,558 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OData.UriParser; +using Microsoft.OData.Edm; +using Microsoft.OData.Tests.UriParser; +using Xunit; +using Microsoft.OData.Core; + +namespace Microsoft.OData.Tests.ScenarioTests.UriParser +{ + /// + /// Tests the CustomUriFunctions class. + /// + public partial class CustomUriFunctionsTests + { + private FunctionSignatureWithReturnType lengthSignature = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetInt32(false), + EdmCoreModel.Instance.GetString(true)); + + [Fact] + public void AddCustomFunction_ForModel_ParmetersCannotBeNull() + { + // Model is null + IEdmModel model = null; + Action test = () => model.AddCustomUriFunction("my.MyNullCustomFunction", null); + Assert.Throws("model", test); + + model = EdmCoreModel.Instance; + // function name is null or empty + test = () => model.AddCustomUriFunction(null, null); + Assert.Throws("functionName", test); + + test = () => model.AddCustomUriFunction(string.Empty, null); + Assert.Throws("functionName", test); + + // function signature is null + test = () => model.AddCustomUriFunction("my.MyNullCustomFunction", null); + Assert.Throws("functionSignature", test); + } + + [Fact] + public void AddCustomFunction_ForModel_CannotAddFunctionSignatureWithNullReturnType() + { + FunctionSignatureWithReturnType customFunctionSignatureWithNullReturnType = new FunctionSignatureWithReturnType(null, EdmCoreModel.Instance.GetInt32(false)); + Action test = () => EdmCoreModel.Instance.AddCustomUriFunction("my.customFunctionWithNoReturnType", customFunctionSignatureWithNullReturnType); + Assert.Throws("functionSignatureWithReturnType must contain a return type", test); + } + + [Fact] + public void AddCustomFunction_ForModel_CannotAddFunctionWhichAlreadyExistsAsBuiltInWithSameFullSignature_AddAsOverload() + { + EdmModel model = new EdmModel(); + Action test = () => model.AddCustomUriFunction("length", lengthSignature); + test.Throws(Error.Format(SRResources.CustomUriFunctions_AddCustomUriFunction_BuiltInExistsFullSignature, "length")); + } + + [Fact] + public void AddCustomFunction_ForModel_ShouldAddFunctionWhichAlreadyExistsAsBuiltInWithSameName_AddAsOverload() + { + EdmModel model = new EdmModel(); + string functionName = "length"; + + FunctionSignatureWithReturnType customFunctionSignature = + new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetDouble(false), + EdmCoreModel.Instance.GetBoolean(false)); + + // Add with 'addAsOverload' 'true' + model.AddCustomUriFunction(functionName, customFunctionSignature); + + FunctionSignatureWithReturnType[] resultFunctionSignaturesWithReturnType = GetCustomFunctionSignaturesOrNull(model, functionName); + + // Assert + Assert.NotNull(resultFunctionSignaturesWithReturnType); + Assert.Single(resultFunctionSignaturesWithReturnType); + Assert.Same(customFunctionSignature, resultFunctionSignaturesWithReturnType[0]); + } + + // Existing Custom Function + [Fact] + public void AddCustomFunction_ForModel_CannotAddFunctionWithFullSignatureExistsAsCustomFunction() + { + EdmModel model = new EdmModel(); + string customFunctionName = "my.ExistingCustomFunction"; + + // Prepare + var existingCustomFunctionSignature = new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetDouble(false), EdmCoreModel.Instance.GetBoolean(false)); + model.AddCustomUriFunction(customFunctionName, existingCustomFunctionSignature); + + // Test + var newCustomFunctionSignature = new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetDouble(false), EdmCoreModel.Instance.GetBoolean(false)); + Action addCustomFunction = () => model.AddCustomUriFunction(customFunctionName, newCustomFunctionSignature); + + // Assert + addCustomFunction.Throws(Error.Format(SRResources.CustomUriFunctions_AddCustomUriFunction_CustomFunctionOverloadExists, customFunctionName)); + } + + [Fact] + public void AddCustomFunction_ForModel_CannotAddFunctionWithFullSignatureExistsAsCustomFunction_AddAsOverload() + { + EdmModel model = new EdmModel(); + string customFunctionName = "my.ExistingCustomFunction"; + + // Prepare + var existingCustomFunctionSignature = new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetDouble(false), EdmCoreModel.Instance.GetBoolean(false)); + model.AddCustomUriFunction(customFunctionName, existingCustomFunctionSignature); + + // Test + var newCustomFunctionSignature = new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetDouble(false), EdmCoreModel.Instance.GetBoolean(false)); + + Action addCustomFunction = () => model.AddCustomUriFunction(customFunctionName, newCustomFunctionSignature); + + // Asserts + addCustomFunction.Throws(Error.Format(SRResources.CustomUriFunctions_AddCustomUriFunction_CustomFunctionOverloadExists, customFunctionName)); + } + + [Fact] + public void AddCustomFunction_ForModel_CustomFunctionDoesntExist_ShouldAdd() + { + EdmModel model = new EdmModel(); + string customFunctionName = "my.NewCustomFunction"; + + // New not existing custom function + var newCustomFunctionSignature = + new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetDouble(false), EdmCoreModel.Instance.GetInt32(false), EdmCoreModel.Instance.GetBoolean(false)); + model.AddCustomUriFunction(customFunctionName, newCustomFunctionSignature); + + // Assert + // Make sure both signatures exists + FunctionSignatureWithReturnType[] customFunctionSignatures = GetCustomFunctionSignaturesOrNull(model, customFunctionName); + + Assert.Single(customFunctionSignatures); + Assert.Same(newCustomFunctionSignature, customFunctionSignatures[0]); + } + + [Fact] + public void AddCustomFunction_ForModel_CustomFunctionDoesntExist_ShouldAdd_NoArgumnetsToFunctionSignature() + { + EdmModel model = new EdmModel(); + string customFunctionName = "my.NewCustomFunction"; + + // New not existing custom function - function without any argumnets + var newCustomFunctionSignature = new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetDouble(false)); + model.AddCustomUriFunction(customFunctionName, newCustomFunctionSignature); + + // Assert + // Make sure both signatures exists + FunctionSignatureWithReturnType[] customFunctionSignatures = GetCustomFunctionSignaturesOrNull(model, customFunctionName); + + Assert.Single(customFunctionSignatures); + Assert.Same(newCustomFunctionSignature, customFunctionSignatures[0]); + } + + [Fact] + public void AddCustomFunction_ForModel_CustomFunctionNameExistsButNotFullSignature_ShouldAddAsAnOverload() + { + EdmModel model = new EdmModel(); + string customFunctionName = "my.ExistingCustomFunction"; + + // Prepare + FunctionSignatureWithReturnType existingCustomFunctionSignature = new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetDouble(false), EdmCoreModel.Instance.GetBoolean(false)); + model.AddCustomUriFunction(customFunctionName, existingCustomFunctionSignature); + + //Test + // Same name, but different signature + var newCustomFunctionSignature = new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetDouble(false), EdmCoreModel.Instance.GetInt32(false), EdmCoreModel.Instance.GetBoolean(false)); + model.AddCustomUriFunction(customFunctionName, newCustomFunctionSignature); + + // Assert + // Make sure both signatures exists + bool areSiganturesAdded = GetCustomFunctionSignaturesOrNull(model,customFunctionName).All(x => x.Equals(existingCustomFunctionSignature) || x.Equals(newCustomFunctionSignature)); + + Assert.True(areSiganturesAdded); + } + + #region Remove Custom Function + + // Validation + + #region Validation + + [Fact] + public void RemoveCustomFunction_ForModel_ParmetersCannotBeNull() + { + // Model is null + IEdmModel model = null; + Assert.Throws("model", () => model.RemoveCustomUriFunction(null, null)); + + // function name is empty or null + model = EdmCoreModel.Instance; + Assert.Throws("functionName", () => model.RemoveCustomUriFunction(null, null)); + Assert.Throws("functionName", () => model.RemoveCustomUriFunction(string.Empty, null)); + + // function signature is null + Assert.Throws("functionSignature", () => model.RemoveCustomUriFunction("FunctionName", null)); + } + + + [Fact] + public void RemoveCustomFunction_ForModel_FunctionSignatureWithoutAReturnType() + { + EdmModel model = new EdmModel(); + FunctionSignatureWithReturnType existingCustomFunctionSignature = + new FunctionSignatureWithReturnType(null, EdmCoreModel.Instance.GetBoolean(false)); + + // Test + Action removeFunction = () => model.RemoveCustomUriFunction("FunctionName", existingCustomFunctionSignature); + + // Assert + Assert.Throws("functionSignatureWithReturnType must contain a return type", removeFunction); + } + + #endregion + + // Remove existing + [Fact] + public void RemoveCustomFunction_ForModel_ShouldRemoveAnExistingFunction_ByName() + { + EdmModel model = new EdmModel(); + string customFunctionName = "my.ExistingCustomFunction"; + + // Prepare + FunctionSignatureWithReturnType existingCustomFunctionSignature = + new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetDouble(false), EdmCoreModel.Instance.GetBoolean(false)); + model.AddCustomUriFunction(customFunctionName, existingCustomFunctionSignature); + + Assert.True(GetCustomFunctionSignaturesOrNull(model, customFunctionName)[0].Equals(existingCustomFunctionSignature)); + + // Test + bool isRemoveSucceeded = model.RemoveCustomUriFunction(customFunctionName); + + // Assert + Assert.True(isRemoveSucceeded); + Assert.Null(GetCustomFunctionSignaturesOrNull(model, customFunctionName)); + } + + // Remove not existing + [Fact] + public void RemoveCustomFunction_ForModel_CannotRemoveFunctionWhichDoesntExist_ByName() + { + EdmModel model = new EdmModel(); + string customFunctionName = "my.ExistingCustomFunction"; + + // Test + bool isRemoveSucceeded = model.RemoveCustomUriFunction(customFunctionName); + + // Assert + Assert.False(isRemoveSucceeded); + } + + // Remove signature, function name doesn't exist + [Fact] + public void RemoveCustomFunction_ForModel_CannotRemoveFunctionWhichDoesntExist_ByNameAndSignature() + { + EdmModel model = new EdmModel(); + string customFunctionName = "my.ExistingCustomFunction"; + FunctionSignatureWithReturnType customFunctionSignature = + new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetDouble(false), EdmCoreModel.Instance.GetBoolean(false)); + + // Test + bool isRemoveSucceeded = model.RemoveCustomUriFunction(customFunctionName, customFunctionSignature); + + // Assert + Assert.False(isRemoveSucceeded); + } + + // Remove signature, function name exists, signature doesn't + [Fact] + public void RemoveCustomFunction_ForModel_CannotRemoveFunctionWithSameNameAndDifferentSignature() + { + EdmModel model = new EdmModel(); + string customFunctionName = "my.ExistingCustomFunction"; + + // Prepare + FunctionSignatureWithReturnType existingCustomFunctionSignature = + new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetDouble(false), EdmCoreModel.Instance.GetBoolean(false)); + model.AddCustomUriFunction(customFunctionName, existingCustomFunctionSignature); + + Assert.True(GetCustomFunctionSignaturesOrNull(model, customFunctionName)[0].Equals(existingCustomFunctionSignature)); + + // Function with different siganture + FunctionSignatureWithReturnType customFunctionSignatureToRemove = + new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetInt16(false), EdmCoreModel.Instance.GetBoolean(false)); + + // Try Remove a function with the same name but different siganture + bool isRemoveSucceeded = model.RemoveCustomUriFunction(customFunctionName, customFunctionSignatureToRemove); + + // Assert + Assert.False(isRemoveSucceeded); + } + + // Remove signature, function and signature exists + [Fact] + public void RemoveCustomFunction_ForModel_RemoveFunctionWithSameNameAndSignature() + { + EdmModel model = new EdmModel(); + string customFunctionName = "my.ExistingCustomFunction"; + + // Prepare + FunctionSignatureWithReturnType existingCustomFunctionSignature = + new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetDouble(false), EdmCoreModel.Instance.GetBoolean(false)); + model.AddCustomUriFunction(customFunctionName, existingCustomFunctionSignature); + + Assert.True(GetCustomFunctionSignaturesOrNull(model, customFunctionName)[0].Equals(existingCustomFunctionSignature)); + + // Test + bool isRemoveSucceeded = model.RemoveCustomUriFunction(customFunctionName, existingCustomFunctionSignature); + + // Assert + Assert.True(isRemoveSucceeded); + + Assert.Null(GetCustomFunctionSignaturesOrNull(model, customFunctionName)); + + } + + // Remove one overload + [Fact] + public void RemoveCustomFunction_ForModel_RemoveFunctionWithSameNameAndSignature_OtherOverloadsExists() + { + EdmModel model = new EdmModel(); + string customFunctionName = "my.ExistingCustomFunction"; + + // Prepare + FunctionSignatureWithReturnType existingCustomFunctionSignature = + new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetDouble(false), EdmCoreModel.Instance.GetBoolean(false)); + model.AddCustomUriFunction(customFunctionName, existingCustomFunctionSignature); + + FunctionSignatureWithReturnType existingCustomFunctionSignatureTwo = + new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetBoolean(false), EdmCoreModel.Instance.GetDate(false)); + model.AddCustomUriFunction(customFunctionName, existingCustomFunctionSignatureTwo); + + // Validate that the two overloads as + Assert.True(GetCustomFunctionSignaturesOrNull(model,customFunctionName). + All(funcSignature => funcSignature.Equals(existingCustomFunctionSignature) || + funcSignature.Equals(existingCustomFunctionSignatureTwo))); + + // Remove the first overload, second overload should not be removed + bool isRemoveSucceeded = model.RemoveCustomUriFunction(customFunctionName, existingCustomFunctionSignature); + + // Assert + Assert.True(isRemoveSucceeded); + + FunctionSignatureWithReturnType[] overloads = GetCustomFunctionSignaturesOrNull(model, customFunctionName); + Assert.Single(overloads); + Assert.Same(existingCustomFunctionSignatureTwo, overloads[0]); + } + + #endregion + + #region ODataUriParser + private static EdmModel BuildNewModel(out IEdmEntityType outPerson, out IEdmStructuralProperty outName, out IEdmEntitySet outPeople) + { + EdmModel model = new EdmModel(); + EdmEntityType person = new EdmEntityType("NS", "Person"); + person.AddKeys(person.AddStructuralProperty("Id", EdmCoreModel.Instance.GetInt32(false))); + outName = person.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(true)); + model.AddElement(person); + + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + outPeople = container.AddEntitySet("People", person); + model.AddElement(container); + outPerson = person; + return model; + } + + [Fact] + public void ParseWithCustomUriFunction_ForModel() + { + IEdmModel model = BuildNewModel(out IEdmEntityType person, out IEdmStructuralProperty nameProp, out IEdmEntitySet entitySet); + + FunctionSignatureWithReturnType myStringFunction + = new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetBoolean(true), EdmCoreModel.Instance.GetString(true), EdmCoreModel.Instance.GetString(true)); + + // Add a custom uri function + model.AddCustomUriFunction("mystringfunction", myStringFunction); + + var fullUri = new Uri("http://www.odata.com/OData/People" + "?$filter=mystringfunction(Name, 'BlaBla')"); + ODataUriParser parser = new ODataUriParser(model, new Uri("http://www.odata.com/OData/"), fullUri); + + var startsWithArgs = parser.ParseFilter().Expression.ShouldBeSingleValueFunctionCallQueryNode("mystringfunction").Parameters.ToList(); + startsWithArgs[0].ShouldBeSingleValuePropertyAccessQueryNode(nameProp); + startsWithArgs[1].ShouldBeConstantQueryNode("BlaBla"); + } + + [Fact] + public void ParseWithMixedCaseCustomUriFunction_ForModel_EnableCaseInsensitive_ShouldWork() + { + IEdmModel model = BuildNewModel(out IEdmEntityType person, out IEdmStructuralProperty nameProp, out IEdmEntitySet entitySet); + + FunctionSignatureWithReturnType myStringFunction + = new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetBoolean(true), EdmCoreModel.Instance.GetString(true), EdmCoreModel.Instance.GetString(true)); + + // Add a custom uri function + model.AddCustomUriFunction("myFirstMixedCasestringfunction", myStringFunction); + + // Uri with mixed-case, should work for resolver with case insensitive enabled. + var fullUri = new Uri("http://www.odata.com/OData/People" + "?$filter=mYFirstMixedCasesTrInGfUnCtIoN(Name, 'BlaBla')"); + ODataUriParser parser = new ODataUriParser(model, new Uri("http://www.odata.com/OData/"), fullUri); + parser.Resolver.EnableCaseInsensitive = true; + + var startsWithArgs = parser.ParseFilter().Expression.ShouldBeSingleValueFunctionCallQueryNode("myFirstMixedCasestringfunction") + .Parameters.ToList(); + startsWithArgs[0].ShouldBeSingleValuePropertyAccessQueryNode(nameProp); + startsWithArgs[1].ShouldBeConstantQueryNode("BlaBla"); + } + + [Fact] + public void ParseWithExactMatchCustomUriFunction_ForModel_EnableCaseInsensitive_ShouldWorkForMultipleEquivalentArgumentsMatches() + { + IEdmModel model = BuildNewModel(out IEdmEntityType person, out IEdmStructuralProperty nameProp, out IEdmEntitySet entitySet); + string lowerCaseName = "myfunction"; + string upperCaseName = lowerCaseName.ToUpper(); + + FunctionSignatureWithReturnType myStringFunction + = new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetBoolean(true), EdmCoreModel.Instance.GetString(true), EdmCoreModel.Instance.GetString(true)); + + // Add two customer uri functions with same argument types, with names different in cases. + model.AddCustomUriFunction(lowerCaseName, myStringFunction); + model.AddCustomUriFunction(upperCaseName, myStringFunction); + string rootUri = "http://www.odata.com/OData/"; + string uriTemplate = rootUri + "People?$filter={0}(Name,'BlaBla')"; + + foreach (string functionName in new string[] { lowerCaseName, upperCaseName }) + { + // Uri with case-sensitive function names referring to equivalent-argument-typed functions, + // should work for resolver with case insensitive enabled. + var fullUri = new Uri(string.Format(uriTemplate, functionName)); + ODataUriParser parser = new ODataUriParser(model, new Uri(rootUri), fullUri); + parser.Resolver.EnableCaseInsensitive = true; + + var startsWithArgs = parser.ParseFilter().Expression.ShouldBeSingleValueFunctionCallQueryNode(functionName).Parameters.ToList(); + startsWithArgs[0].ShouldBeSingleValuePropertyAccessQueryNode(nameProp); + startsWithArgs[1].ShouldBeConstantQueryNode("BlaBla"); + } + } + + [Fact] + public void ParseWithCustomUriFunction_ForModel_EnableCaseInsensitive_ShouldThrowDueToAmbiguity() + { + IEdmModel model = BuildNewModel(out IEdmEntityType person, out IEdmStructuralProperty nameProp, out IEdmEntitySet entitySet); + string lowerCaseName = "myfunction"; + string upperCaseName = lowerCaseName.ToUpper(); + + FunctionSignatureWithReturnType myStringFunction + = new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetBoolean(true), EdmCoreModel.Instance.GetString(true), EdmCoreModel.Instance.GetString(true)); + + // Add two customer uri functions with same argument types, with names different in cases. + model.AddCustomUriFunction(lowerCaseName, myStringFunction); + model.AddCustomUriFunction(upperCaseName, myStringFunction); + string rootUri = "http://www.odata.com/OData/"; + string uriTemplate = rootUri + "People?$filter={0}(Name,'BlaBla')"; + + int strLen = lowerCaseName.Length; + string mixedCaseFunctionName = lowerCaseName.Substring(0, strLen / 2).ToUpper() + lowerCaseName.Substring(strLen / 2); + // Uri with mix-case function names referring to equivalent-argument-typed functions, + // should result in exception for resolver with case insensitive enabled due to ambiguity (multiple equivalent matches). + var fullUri = new Uri(string.Format(uriTemplate, mixedCaseFunctionName)); + ODataUriParser parser = new ODataUriParser(model, new Uri(rootUri), fullUri); + parser.Resolver.EnableCaseInsensitive = true; + + Action action = () => parser.ParseFilter(); + Assert.Throws(action); + } + + [Fact] + public void ParseWithMixedCaseCustomUriFunction_ForModel_DisableCaseInsensitive_ShouldFailed() + { + IEdmModel model = BuildNewModel(out IEdmEntityType person, out IEdmStructuralProperty nameProp, out IEdmEntitySet entitySet); + bool exceptionThrown = false; + try + { + FunctionSignatureWithReturnType myStringFunction + = new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetBoolean(true), + EdmCoreModel.Instance.GetString(true), EdmCoreModel.Instance.GetString(true)); + + // Add a custom uri function + model.AddCustomUriFunction("myMixedCasestringfunction", myStringFunction); + + // Uri with mixed-case, should fail for default resolver with case-insensitive disabled. + var fullUri = new Uri("http://www.odata.com/OData/People" + "?$filter=mYMixedCasesTrInGfUnCtIoN(Name, 'BlaBla')"); + ODataUriParser parser = new ODataUriParser(model, new Uri("http://www.odata.com/OData/"), fullUri); + parser.Resolver.EnableCaseInsensitive = false; + + parser.ParseFilter(); + } + catch (ODataException e) + { + Assert.Equal("An unknown function with name 'mYMixedCasesTrInGfUnCtIoN' was found. " + + "This may also be a function import or a key lookup on a navigation property, which is not allowed.", e.Message); + exceptionThrown = true; + } + + Assert.True(exceptionThrown, "Exception should be thrown trying to parse mixed-case uri function when case-insensitive is disabled."); + } + + [Fact] + public void ParseWithCustomUriFunction_ForModel_AddAsOverloadToBuiltIn() + { + IEdmModel model = BuildNewModel(out IEdmEntityType person, out IEdmStructuralProperty nameProp, out IEdmEntitySet entitySet); + FunctionSignatureWithReturnType customStartWithFunctionSignature = + new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetBoolean(true), + EdmCoreModel.Instance.GetString(true), + EdmCoreModel.Instance.GetInt32(true)); + + // Add with override 'true' + model.AddCustomUriFunction("startswith", customStartWithFunctionSignature); + + var fullUri = new Uri("http://www.odata.com/OData/People" + "?$filter=startswith(Name, 66)"); + ODataUriParser parser = new ODataUriParser(model, new Uri("http://www.odata.com/OData/"), fullUri); + + var startsWithArgs = parser.ParseFilter().Expression.ShouldBeSingleValueFunctionCallQueryNode("startswith").Parameters.ToList(); + startsWithArgs[0].ShouldBeSingleValuePropertyAccessQueryNode(nameProp); + startsWithArgs[1].ShouldBeConstantQueryNode(66); + } + + [Fact] + public void ParseWithCustomFunction_ForModel_EnumParameter() + { + EdmModel model = BuildNewModel(out IEdmEntityType person, out IEdmStructuralProperty nameProp, out IEdmEntitySet entitySet); + + var enumType = new EdmEnumType("NS", "NonFlagShape", EdmPrimitiveTypeKind.SByte, false); + enumType.AddMember("Rectangle", new EdmEnumMemberValue(1)); + enumType.AddMember("Triangle", new EdmEnumMemberValue(2)); + enumType.AddMember("foursquare", new EdmEnumMemberValue(3)); + var enumTypeRef = new EdmEnumTypeReference(enumType, false); + + FunctionSignatureWithReturnType signature = + new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetBoolean(false), enumTypeRef); + + model.AddCustomUriFunction("enumFunc", signature); + + var fullUri = new Uri("http://www.odata.com/OData/People" + "?$filter=enumFunc('Rectangle')"); + ODataUriParser parser = new ODataUriParser(model, new Uri("http://www.odata.com/OData/"), fullUri); + + var enumFuncWithArgs = parser.ParseFilter().Expression.ShouldBeSingleValueFunctionCallQueryNode("enumFunc").Parameters.ToList(); + enumFuncWithArgs[0].ShouldBeEnumNode(enumType, "Rectangle"); + } + + #endregion + + #region Private Methods + + private FunctionSignatureWithReturnType[] GetCustomFunctionSignaturesOrNull(string customFunctionName) + { + IList> resultFunctionSignaturesWithReturnType = null; + CustomUriFunctions.TryGetCustomFunction(customFunctionName, out resultFunctionSignaturesWithReturnType); + + return resultFunctionSignaturesWithReturnType?.Select( _ => _.Value).ToArray(); + } + #endregion + } +} diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/CustomUriFunctionsTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/CustomUriFunctionsTests.cs index 37bdd9a462..d129b01a19 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/CustomUriFunctionsTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/CustomUriFunctionsTests.cs @@ -18,7 +18,7 @@ namespace Microsoft.OData.Tests.ScenarioTests.UriParser /// /// Tests the CustomUriFunctions class. /// - public class CustomUriFunctionsTests + public partial class CustomUriFunctionsTests { #region Constants @@ -698,10 +698,10 @@ public void ParseWithCustomFunction_EnumParameter() #region Private Methods - private FunctionSignatureWithReturnType[] GetCustomFunctionSignaturesOrNull(string customFunctionName) + private FunctionSignatureWithReturnType[] GetCustomFunctionSignaturesOrNull(IEdmModel model, string customFunctionName) { IList> resultFunctionSignaturesWithReturnType = null; - CustomUriFunctions.TryGetCustomFunction(customFunctionName, out resultFunctionSignaturesWithReturnType); + model.TryGetCustomFunction(customFunctionName, out resultFunctionSignaturesWithReturnType); return resultFunctionSignaturesWithReturnType?.Select( _ => _.Value).ToArray(); }