Skip to content

Commit

Permalink
Fixes #2911, Add APIs to regiter custom uri functions for certain model
Browse files Browse the repository at this point in the history
  • Loading branch information
xuzhg committed Dec 12, 2024
1 parent 70eef8a commit 2a24fac
Show file tree
Hide file tree
Showing 5 changed files with 794 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -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<System.Collections.Generic.KeyValuePair<string, Microsoft.OData.UriParser.FunctionSignatureWithReturnType>> nameSignatures, bool enableCaseInsensitive = false) -> bool
82 changes: 60 additions & 22 deletions src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,19 +146,25 @@ internal static KeyValuePair<string, FunctionSignatureWithReturnType> MatchSigna
/// <param name="enableCaseInsensitive">Optional flag for whether case insensitive match is enabled.</param>
/// <returns>The signatures which match the supplied function name.</returns>
[SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "need to use lower characters for built-in functions.")]
internal static IList<KeyValuePair<string, FunctionSignatureWithReturnType>> GetUriFunctionSignatures(string functionCallToken, bool enableCaseInsensitive = false)
internal static IList<KeyValuePair<string, FunctionSignatureWithReturnType>> GetUriFunctionSignatures(string functionCallToken, IEdmModel model = null, bool enableCaseInsensitive = false)
{
IList<KeyValuePair<string, FunctionSignatureWithReturnType>> customUriFunctionsNameSignatures = null;
string builtInUriFunctionName = null;
FunctionSignatureWithReturnType[] builtInUriFunctionsSignatures = null;
IList<KeyValuePair<string, FunctionSignatureWithReturnType>> 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<KeyValuePair<string, FunctionSignatureWithReturnType>> 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)
Expand All @@ -167,25 +173,57 @@ internal static IList<KeyValuePair<string, FunctionSignatureWithReturnType>> Get
builtInUriFunctionsSignatures.Select(sig => new KeyValuePair<string, FunctionSignatureWithReturnType>(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(
Expand Down Expand Up @@ -300,7 +338,7 @@ private QueryNode BindAsUriFunction(FunctionCallToken functionCallToken, List<Qu
}

// Do some validation and get potential Uri functions that could match what we saw
IList<KeyValuePair<string, FunctionSignatureWithReturnType>> nameSignatures = GetUriFunctionSignatures(functionCallToken.Name,
IList<KeyValuePair<string, FunctionSignatureWithReturnType>> nameSignatures = GetUriFunctionSignatures(functionCallToken.Name, state.Model,
this.state.Configuration.EnableCaseInsensitiveUriFunctionIdentifier);

SingleValueNode[] argumentNodeArray = ValidateArgumentsAreSingleValue(functionCallToken.Name, argumentNodes);
Expand Down
174 changes: 169 additions & 5 deletions src/Microsoft.OData.Core/UriParser/CustomUriFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,6 +18,17 @@ namespace Microsoft.OData.UriParser

#endregion

/// <summary>
/// Class represents the annotation for custom functions
/// </summary>
internal class CustomUriFunctionsAnnotation
{
/// <summary>
/// Dictionary of the name of the custom function and all the signatures.
/// </summary>
public ConcurrentDictionary<string, FunctionSignatureWithReturnType[]> CustomFunctions { get; } = new ConcurrentDictionary<string, FunctionSignatureWithReturnType[]>(StringComparer.Ordinal);
}

/// <summary>
/// Class represents functions signatures of custom uri functions.
/// </summary>
Expand All @@ -36,6 +48,158 @@ private static readonly Dictionary<string, FunctionSignatureWithReturnType[]> Cu

#region Public Methods

/// <summary>
/// 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.
/// </summary>
/// <param name="model">The given Edm model.</param>
/// <param name="functionName">The new custom function name.</param>
/// <param name="functionSignature">The new custom function signature.</param>
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);
}

/// <summary>
/// Removes the specific function overload from the custom uri functions.
/// </summary>
/// <param name="model">The given Edm model.</param>
/// <param name="functionName">Custom function name to remove.</param>
/// <param name="functionSignature">The specific signature overload of the function to remove.</param>
/// <returns>'False' if custom function signature doesn't exist. 'True' if function has been removed successfully.</returns>
/// <exception cref="ArgumentNullException">Arguments are null, or function signature return type is null.</exception>
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<string, FunctionSignatureWithReturnType[]> 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;
}
}

/// <summary>
/// Removes all the function overloads from the custom uri functions.
/// </summary>
/// <param name="model">The given Edm model.</param>
/// <param name="functionName">The custom function name.</param>
/// <returns>'False' if custom function signature doesn't exist. 'True' if function has been removed successfully</returns>
/// <exception cref="ArgumentNullException">Arguments are null, or function signature return type is null</exception>
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 _);
}

/// <summary>
/// Returns a list of name-signature pairs for a function name.
/// </summary>
/// <param name="model">The given Edm model.</param>
/// <param name="functionCallToken">The name of the function to look for.</param>
/// <param name="nameSignatures">
/// Output for the list of signature objects for matched function names, with canonical name of the function;
/// null if no matches found.
/// </param>
/// <param name="enableCaseInsensitive">Whether to perform case-insensitive match for function name.</param>
/// <returns>true if the function was found, or false otherwise.</returns>
public static bool TryGetCustomFunction(this IEdmModel model, string functionCallToken, out IList<KeyValuePair<string, FunctionSignatureWithReturnType>> nameSignatures,
bool enableCaseInsensitive = false)
{
ExceptionUtils.CheckArgumentNotNull(model, "model");

nameSignatures = null;
CustomUriFunctionsAnnotation funAnnotations = model.GetAnnotationValue<CustomUriFunctionsAnnotation>(model);
if (funAnnotations == null)
{
return false;
}

IList<KeyValuePair<string, FunctionSignatureWithReturnType>> bufferedKeyValuePairs
= new List<KeyValuePair<string, FunctionSignatureWithReturnType>>();

foreach (KeyValuePair<string, FunctionSignatureWithReturnType[]> func in funAnnotations.CustomFunctions)
{
if (func.Key.Equals(functionCallToken, enableCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
{
foreach (FunctionSignatureWithReturnType sig in func.Value)
{
bufferedKeyValuePairs.Add(new KeyValuePair<string, FunctionSignatureWithReturnType>(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<CustomUriFunctionsAnnotation>(model);
if (annotation == null)
{
annotation = new CustomUriFunctionsAnnotation();
model.SetAnnotationValue(model, annotation);
}

return annotation;
}

/// <summary>
/// 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.
Expand Down Expand Up @@ -71,7 +235,7 @@ public static void AddCustomUriFunction(string functionName, FunctionSignatureWi
}
}

AddCustomFunction(functionName, functionSignature);
AddCustomFunction(CustomFunctions, functionName, functionSignature);
}
}

Expand Down Expand Up @@ -184,14 +348,14 @@ IList<KeyValuePair<string, FunctionSignatureWithReturnType>> bufferedKeyValuePai

#region Private Methods

private static void AddCustomFunction(string customFunctionName, FunctionSignatureWithReturnType newCustomFunctionSignature)
private static void AddCustomFunction(IDictionary<string, FunctionSignatureWithReturnType[]> 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
{
Expand All @@ -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();
}
}
Expand Down
Loading

0 comments on commit 2a24fac

Please sign in to comment.