Skip to content
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

Open
wants to merge 1 commit into
base: release-7.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 ")"
Copy link
Contributor

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>...

/// </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 ')'.
Copy link
Contributor

Choose a reason for hiding this comment

The 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*/);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Named parameters would be neater...

bool paramertersFound = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
bool paramertersFound = false;
bool parametersFound = false;

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>
Expand Down
46 changes: 38 additions & 8 deletions src/Microsoft.OData.Core/UriParser/Parsers/ODataPathParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we know (1) represents the key and not the parameters?

if (parenthesisKey == null && resolvedParameters == null)
{
parenthesisKey = parenthesisExpression;
}

this.TryBindKeySegmentIfNoResolvedParametersAndParenthesisValueExists(parenthesisKey, returnType, segment);

return true;
}
Expand All @@ -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))
Expand Down Expand Up @@ -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))
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1047,9 +1047,14 @@ public void KeyLookupCanAppearAfterComposableFunctionWithoutParameters()
}

[Fact]
public void KeyLookupCannotAppearAfterFunctionWithParameters()
public void KeyLookupCanAppearAfterFunctionWithParameters()
{
PathFunctionalTestsUtil.RunParseErrorPath("People(1)/Fully.Qualified.Namespace.AllMyFriendsDogs(inOffice=true)(1)", ODataErrorStrings.ExpressionLexer_SyntaxError(14, "inOffice=true)(1"));
var path = PathFunctionalTestsUtil.RunParsePath("People(1)/Fully.Qualified.Namespace.AllMyFriendsDogs(inOffice=true)(42)");
Assert.Equal(4, path.Count);
path.Segments[0].ShouldBeEntitySetSegment(HardCodedTestModel.GetPeopleSet());
path.Segments[1].ShouldBeKeySegment(new KeyValuePair<string, object>("ID", 1));
path.Segments[2].ShouldBeOperationSegment(HardCodedTestModel.GetFunctionForAllMyFriendsDogs(2));
path.Segments[3].ShouldBeKeySegment(new KeyValuePair<string, object>("ID", 42));
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2232,9 +2232,9 @@ public static IEdmFunctionImport GetFunctionImportIsAddressGood()
return TestModel.EntityContainer.FindOperationImports("IsAddressGood").Single() as IEdmFunctionImport;
}

public static IEdmFunction GetFunctionForAllMyFriendsDogs()
public static IEdmFunction GetFunctionForAllMyFriendsDogs(int parameterCount = 1)
{
return TestModel.FindOperations("Fully.Qualified.Namespace.AllMyFriendsDogs").Single(f => f.Parameters.Count() == 1) as IEdmFunction;
return TestModel.FindOperations("Fully.Qualified.Namespace.AllMyFriendsDogs").Single(f => f.Parameters.Count() == parameterCount) as IEdmFunction;
}

public static IEdmOperationImport[] GetAllFunctionImportsForGetMostImportantPerson()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ namespace Microsoft.OData.Tests.UriParser.Parsers
/// </summary>
public class FunctionParameterParserTests
{
[Theory]
[InlineData("1", "1", null)]
Copy link
Contributor

Choose a reason for hiding this comment

The 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'")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add the "" scenario? No parameters and no keys, i.e. fn()

public void SplitOperationParametersAndParenthesisKey_WorksForInputExpression(string expression, string parameters, string keys)
{
FunctionParameterParser.SplitOperationParametersAndParenthesisKey(expression, out string acutalParams, out string actualKeys);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
FunctionParameterParser.SplitOperationParametersAndParenthesisKey(expression, out string acutalParams, out string actualKeys);
FunctionParameterParser.SplitOperationParametersAndParenthesisKey(expression, out string actualParams, out string actualKeys);

Assert.Equal(parameters, acutalParams);
Assert.Equal(keys, actualKeys);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test for the scenario where the ODataException(ODataErrorStrings.ExpressionLexer_SyntaxError(currentToken.Position, parenthesisExpression)); is thrown

[Fact]
public void FunctionParameterParserShouldSupportUnresolvedAliasesInPath()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,30 @@ public void ParseCountAfterServiceRootShouldFail()
parsePath.Throws<ODataUnrecognizedPathException>("The request URI is not valid, the segment $count cannot be applied to the root of the service.");
}

[Fact]
public void ParseKeyInParenthesisAfterBoundFunctionReturnsCollectionOfEntitiesShouldWork()
{
// People(1)/Fully.Qualified.Namespace.AllMyFriendsDogs(inOffice = true)(42)
IList<ODataPathSegment> path = this.testSubject.ParsePath(new[] { "People(1)", "Fully.Qualified.Namespace.AllMyFriendsDogs(inOffice=true)(42)" });
Assert.Equal(4, path.Count);
path[0].ShouldBeEntitySetSegment(HardCodedTestModel.GetPeopleSet());
path[1].ShouldBeKeySegment(new KeyValuePair<string, object>("ID", 1));
path[2].ShouldBeOperationSegment(HardCodedTestModel.GetFunctionForAllMyFriendsDogs(2));
path[3].ShouldBeKeySegment(new KeyValuePair<string, object>("ID", 42));
}

[Fact]
public void ParseKeyAsSegmentAfterBoundFunctionReturnsCollectionOfEntitiesShouldWork()
{
// People(1)/Fully.Qualified.Namespace.AllMyFriendsDogs(inOffice = true)/42
IList<ODataPathSegment> path = this.testSubject.ParsePath(new[] { "People(1)", "Fully.Qualified.Namespace.AllMyFriendsDogs(inOffice=true)", "42" });
Assert.Equal(4, path.Count);
path[0].ShouldBeEntitySetSegment(HardCodedTestModel.GetPeopleSet());
path[1].ShouldBeKeySegment(new KeyValuePair<string, object>("ID", 1));
path[2].ShouldBeOperationSegment(HardCodedTestModel.GetFunctionForAllMyFriendsDogs(2));
path[3].ShouldBeKeySegment(new KeyValuePair<string, object>("ID", 42));
}

[Fact]
public void ParseBoundFunctionWithTypeDefinitionAsParameterAndReturnTypeShouldWork()
{
Expand Down