-
Notifications
You must be signed in to change notification settings - Fork 349
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixes #2754, enable key in parenthesis after composable function with parameters #2757
base: release-7.x
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -50,6 +50,82 @@ internal static bool TrySplitOperationParameters(string parenthesisExpression, O | |||||
return ret; | ||||||
} | ||||||
|
||||||
/// <summary> | ||||||
/// Splits the parenthesis expression into two parts (if apply) | ||||||
/// One is the function parameter, the other is key in parenthesis (if exists) | ||||||
/// Be noted, the input expression doesn't contain the beginning "(" and the ending ")" | ||||||
/// </summary> | ||||||
/// <param name="parenthesisExpression">the input expression</param> | ||||||
/// <param name="parameters">the output for parameter part</param> | ||||||
/// <param name="parenthesisKey">the output for key in parenthesis part</param> | ||||||
internal static void SplitOperationParametersAndParenthesisKey(string parenthesisExpression, out string parameters, out string parenthesisKey) | ||||||
{ | ||||||
// Be noted, the input expression doesn't contain the first '(' and last ')'. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we have a sanity check to confirm that the input expression does not containe the beginning "(" and the ending ")"? |
||||||
// for example | ||||||
// "degree=')')('fawn'" | ||||||
// ")('fawn'" ==> empty parameter with a key in parenthesis | ||||||
parameters = parenthesisExpression; | ||||||
parenthesisKey = null; | ||||||
|
||||||
if (string.IsNullOrEmpty(parenthesisExpression)) | ||||||
{ | ||||||
return; | ||||||
} | ||||||
|
||||||
Stack<ExpressionTokenKind> stack = new Stack<ExpressionTokenKind>(); | ||||||
stack.Push(ExpressionTokenKind.OpenParen); | ||||||
ExpressionLexer lexer = new ExpressionLexer(parenthesisExpression, true /*moveToFirstToken*/, false /*useSemicolonDelimiter*/, true /*parsingFunctionParameters*/); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Named parameters would be neater... |
||||||
bool paramertersFound = false; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Suggested change
|
||||||
bool parenthesisKeyFound = false; | ||||||
int parenthesisKeyStartPosition = 0; | ||||||
ExpressionToken currentToken = lexer.CurrentToken; | ||||||
while (true) | ||||||
{ | ||||||
if (currentToken.Kind == ExpressionTokenKind.OpenParen) | ||||||
{ | ||||||
if (stack.Count == 0) | ||||||
{ | ||||||
parenthesisKeyStartPosition = currentToken.Position; | ||||||
} | ||||||
|
||||||
stack.Push(ExpressionTokenKind.OpenParen); | ||||||
} | ||||||
else if (currentToken.Kind == ExpressionTokenKind.CloseParen || currentToken.Kind == ExpressionTokenKind.End) | ||||||
{ | ||||||
if (stack.Count == 1) // It's a top level | ||||||
{ | ||||||
if (!paramertersFound) | ||||||
{ | ||||||
parameters = parenthesisExpression.Substring(0, currentToken.Position); | ||||||
paramertersFound = true; | ||||||
} | ||||||
else if (!parenthesisKeyFound) | ||||||
{ | ||||||
parenthesisKeyFound = true; | ||||||
} | ||||||
else | ||||||
{ | ||||||
throw new ODataException(ODataErrorStrings.ExpressionLexer_SyntaxError(currentToken.Position, parenthesisExpression)); | ||||||
} | ||||||
} | ||||||
|
||||||
stack.Pop(); // match an embeded '()' | ||||||
} | ||||||
|
||||||
if (currentToken.Kind == ExpressionTokenKind.End) | ||||||
{ | ||||||
break; | ||||||
} | ||||||
|
||||||
currentToken = lexer.NextToken(); | ||||||
} | ||||||
|
||||||
if (parenthesisKeyFound) | ||||||
{ | ||||||
parenthesisKey = parenthesisExpression.Substring(parenthesisKeyStartPosition + 1);// +1 means to remove the leading '(' | ||||||
} | ||||||
} | ||||||
|
||||||
/// <summary> | ||||||
/// Tries to parse a collection of function parameters. Allows path and filter to share the core algorithm while representing parameters differently. | ||||||
/// </summary> | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1053,6 +1053,11 @@ private bool TryCreateSegmentForNavigationSource(string identifier, string paren | |
[SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "IEdmModel", Justification = "The spelling is correct.")] | ||
private bool TryCreateSegmentForOperationImport(string identifier, string parenthesisExpression) | ||
{ | ||
FunctionParameterParser.SplitOperationParametersAndParenthesisKey(parenthesisExpression, | ||
out string newParenthesisParameters, | ||
out string parenthesisKey); | ||
parenthesisExpression = newParenthesisParameters; | ||
|
||
ICollection<OperationSegmentParameter> resolvedParameters; | ||
IEdmOperationImport singleImport; | ||
if (!TryBindingParametersAndMatchingOperationImport(identifier, parenthesisExpression, this.configuration, out resolvedParameters, out singleImport)) | ||
|
@@ -1072,7 +1077,14 @@ private bool TryCreateSegmentForOperationImport(string identifier, string parent | |
|
||
this.parsedSegments.Add(segment); | ||
|
||
this.TryBindKeySegmentIfNoResolvedParametersAndParenthesisValueExists(parenthesisExpression, returnType, resolvedParameters, segment); | ||
// Be noted, it's back-compatibile since the function can be called without parameters but with keys | ||
// for example: "~/GetCoolPeople(1)", where '1' is the key, not the parameters. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We know that "1" is a key and not a parameter because it's not named, no? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do we know |
||
if (parenthesisKey == null && resolvedParameters == null) | ||
{ | ||
parenthesisKey = parenthesisExpression; | ||
} | ||
|
||
this.TryBindKeySegmentIfNoResolvedParametersAndParenthesisValueExists(parenthesisKey, returnType, segment); | ||
|
||
return true; | ||
} | ||
|
@@ -1082,12 +1094,11 @@ private bool TryCreateSegmentForOperationImport(string identifier, string parent | |
/// </summary> | ||
/// <param name="parenthesisExpression">The parenthesis expression.</param> | ||
/// <param name="returnType">Type of the return.</param> | ||
/// <param name="resolvedParameters">The resolved parameters.</param> | ||
/// <param name="segment">The segment.</param> | ||
private void TryBindKeySegmentIfNoResolvedParametersAndParenthesisValueExists(string parenthesisExpression, IEdmTypeReference returnType, ICollection<OperationSegmentParameter> resolvedParameters, ODataPathSegment segment) | ||
private void TryBindKeySegmentIfNoResolvedParametersAndParenthesisValueExists(string parenthesisExpression, IEdmTypeReference returnType, ODataPathSegment segment) | ||
{ | ||
IEdmCollectionTypeReference collectionTypeReference = returnType as IEdmCollectionTypeReference; | ||
if (collectionTypeReference != null && collectionTypeReference.ElementType().IsEntity() && resolvedParameters == null && parenthesisExpression != null) | ||
if (collectionTypeReference != null && collectionTypeReference.ElementType().IsEntity() && parenthesisExpression != null) | ||
{ | ||
// The parameters in the parenthesis is a key segment. | ||
if (this.TryBindKeyFromParentheses(parenthesisExpression)) | ||
|
@@ -1115,6 +1126,11 @@ private bool TryCreateSegmentForOperation(ODataPathSegment previousSegment, stri | |
bindingType = (previousSegment is EachSegment) ? previousSegment.TargetEdmType : previousSegment.EdmType; | ||
} | ||
|
||
FunctionParameterParser.SplitOperationParametersAndParenthesisKey(parenthesisExpression, | ||
out string newParenthesisParameters, | ||
out string parenthesisKey); | ||
parenthesisExpression = newParenthesisParameters; | ||
|
||
ICollection<OperationSegmentParameter> resolvedParameters; | ||
IEdmOperation singleOperation; | ||
if (!TryBindingParametersAndMatchingOperation(identifier, parenthesisExpression, bindingType, this.configuration, out resolvedParameters, out singleOperation)) | ||
|
@@ -1132,12 +1148,14 @@ private bool TryCreateSegmentForOperation(ODataPathSegment previousSegment, stri | |
throw new ODataException(ODataErrorStrings.FunctionCallBinder_CallingFunctionOnOpenProperty(identifier)); | ||
} | ||
|
||
CreateOperationSegment(previousSegment, singleOperation, resolvedParameters, identifier, parenthesisExpression); | ||
CreateOperationSegment(previousSegment, singleOperation, resolvedParameters, identifier, parenthesisExpression, parenthesisKey); | ||
|
||
return true; | ||
} | ||
|
||
private void CreateOperationSegment(ODataPathSegment previousSegment, IEdmOperation singleOperation, ICollection<OperationSegmentParameter> resolvedParameters, string identifier, string parenthesisExpression) | ||
private void CreateOperationSegment(ODataPathSegment previousSegment, IEdmOperation singleOperation, | ||
ICollection<OperationSegmentParameter> resolvedParameters, | ||
string identifier, string parenthesisExpression, string parenthesisKey) | ||
{ | ||
IEdmTypeReference returnType = singleOperation.ReturnType; | ||
IEdmEntitySetBase targetset = null; | ||
|
@@ -1162,7 +1180,15 @@ private void CreateOperationSegment(ODataPathSegment previousSegment, IEdmOperat | |
}; | ||
|
||
this.parsedSegments.Add(segment); | ||
this.TryBindKeySegmentIfNoResolvedParametersAndParenthesisValueExists(parenthesisExpression, returnType, resolvedParameters, segment); | ||
|
||
// Be noted, it's back-compatibile since the function can be called without parameters but with keys | ||
// for example: "~/GetCoolPeople(1)", where '1' is the key, not the parameters. | ||
if (parenthesisKey == null && resolvedParameters == null) | ||
{ | ||
parenthesisKey = parenthesisExpression; | ||
} | ||
|
||
this.TryBindKeySegmentIfNoResolvedParametersAndParenthesisValueExists(parenthesisKey, returnType, segment); | ||
|
||
return; | ||
} | ||
|
@@ -1342,10 +1368,14 @@ private bool TryBindEscapeFunction() | |
IEdmFunction escapeFunction; | ||
if (this.TryResolveEscapeFunction(previous, out newIdentifier, out newParenthesisExpression, out anotherEscapeFunctionStarts, out escapeFunction)) | ||
{ | ||
FunctionParameterParser.SplitOperationParametersAndParenthesisKey(newParenthesisExpression, | ||
out string newParenthesisParameters, out string parenthesisKey); | ||
newParenthesisExpression = newParenthesisParameters; | ||
|
||
ICollection<FunctionParameterToken> splitParameters; | ||
FunctionParameterParser.TrySplitOperationParameters(newParenthesisExpression, configuration, out splitParameters); | ||
ICollection<OperationSegmentParameter> resolvedParameters = FunctionCallBinder.BindSegmentParameters(configuration, escapeFunction, splitParameters); | ||
CreateOperationSegment(previous, escapeFunction, resolvedParameters, newIdentifier, newParenthesisExpression); | ||
CreateOperationSegment(previous, escapeFunction, resolvedParameters, newIdentifier, newParenthesisExpression, parenthesisKey); | ||
if (anotherEscapeFunctionStarts) | ||
{ | ||
// When we encounter an invalid escape function as a parameter, we should throw. | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -21,6 +21,19 @@ namespace Microsoft.OData.Tests.UriParser.Parsers | |||||
/// </summary> | ||||||
public class FunctionParameterParserTests | ||||||
{ | ||||||
[Theory] | ||||||
[InlineData("1", "1", null)] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a scenario where the function is being called with one parameter without specifying the name? |
||||||
[InlineData(")(1", "", "1")] | ||||||
[InlineData("person=People(1)", "person=People(1)", null)] | ||||||
[InlineData("person=People(1))('bca(aa('", "person=People(1)", "'bca(aa('")] | ||||||
[InlineData("degree=1)('fawn'", "degree=1", "'fawn'")] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add the |
||||||
public void SplitOperationParametersAndParenthesisKey_WorksForInputExpression(string expression, string parameters, string keys) | ||||||
{ | ||||||
FunctionParameterParser.SplitOperationParametersAndParenthesisKey(expression, out string acutalParams, out string actualKeys); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Suggested change
|
||||||
Assert.Equal(parameters, acutalParams); | ||||||
Assert.Equal(keys, actualKeys); | ||||||
} | ||||||
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a test for the scenario where the |
||||||
[Fact] | ||||||
public void FunctionParameterParserShouldSupportUnresolvedAliasesInPath() | ||||||
{ | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this should be a
<remark>
...