Repository navigation
One drive escape function for key as segment staring with colon - #1621
Conversation
| parenthesisExpression = null; | ||
| } | ||
| else if (segmentText[segmentText.Length - 1] == ':') | ||
| { |
There was a problem hiding this comment.
add a comment describing logic -- is this to support syntax such as:
entityset(key):/escape-function-path? #Closed
| else if (segmentText[segmentText.Length - 1] == ':') | ||
| { | ||
|
|
||
| if (segmentText.Length < 2 || |
There was a problem hiding this comment.
remove empty blank line #Closed
|
|
||
|
|
||
|
|
||
| return false; |
There was a problem hiding this comment.
remove extra blank lines
#Closed
|
|
||
| bool trybindingEscapeFunction = false; | ||
|
|
||
| if (segmentText.Length > 0 && segmentText[segmentText.Length - 1] == ':' && previous.EdmType != null) |
There was a problem hiding this comment.
segmentText.Length > 0 && [](start = 16, length = 25)
is this ever called with a zero-length segmentText, or should this just be an assert? #Closed
| IEdmType bindingType = null; | ||
| if (previous != null) | ||
| { | ||
| bindingType = (previous is EachSegment) ? previous.TargetEdmType : previous.EdmType; |
There was a problem hiding this comment.
(previous is EachSegment) [](start = 30, length = 25)
Do we have a test to validate this? In general, I don't think we should support an escape function (or any other function) following $each. The only thing that should validly follow $each is a bound action. #Closed
There was a problem hiding this comment.
I have removed the check of $each at this point for escape functions if it should not be supported.
In reply to: 366143523 [](ancestors = 366143523)
| Assert.Equal("xyz/abc", ((ConstantNode)parameter.Value).Value); | ||
| } | ||
|
|
||
| [Fact] |
There was a problem hiding this comment.
Is this test no longer valid? #Closed
|
var odataException = Assert.Throws |
||
| Assert.Equal(ODataErrorStrings.RequestUriProcessor_NoBoundEscapeFunctionSupported("NS.OneDrive"), odataException.Message); | ||
| } | ||
|
|
There was a problem hiding this comment.
What is the behavior for this url? It should still throw an invalid url, right? because root: is not a valid identifier? #Closed
There was a problem hiding this comment.
Correct. Added the test back with the updated error message.
In reply to: 366144030 [](ancestors = 366144030)
| [InlineData("EntitySet('key'):/xyz:/:/perm", new[] { "EntitySet('key')", ":xyz:", ":perm" })] | ||
| [InlineData(":/xyz::/perm", new[] { ":xyz:", ":perm" })] | ||
| [InlineData(":/xyz:/:/perm", new[] { ":xyz:", ":perm" })] | ||
| [InlineData(":/xyz://///:/perm", new[] { ":xyz:", ":perm" })] |
There was a problem hiding this comment.
Why are all variations of this test removed? Aren't most of these still valid? Any that are now invalid should have added negative tests. #Closed
There was a problem hiding this comment.
+1, we can change the expected to make the test valid again.
Because all the patterns are valid case, we can use this test cases to see the ParsePath works expected for these patterns.
In reply to: 366144313 [](ancestors = 366144313)
| [Theory] | ||
| [InlineData("root::/abc", "root::")] | ||
| [InlineData("EntitySet('key')::/abc", "EntitySet('key')::")] | ||
| public void ParseInvalidEscapeUriPathShouldThrow(string pattern, string segment) |
There was a problem hiding this comment.
Are these now both valid? What is the behavior? #Closed
There was a problem hiding this comment.
Yes, both are valid for this step. These will throw an Unrecogonized Path Exception in the E2E scenario while binding.
In reply to: 366144414 [](ancestors = 366144414)
|
|
||
| bool trybindingEscapeFunction = false; | ||
|
|
||
| if (segmentText.Length > 0 && segmentText[segmentText.Length - 1] == ':' && previous.EdmType != null) |
There was a problem hiding this comment.
segmentText.Length > 0 [](start = 16, length = 22)
Should this be an assert? Didn't we validate that segmentText.Length > 0 in ExtractSegmentIdentifierAndParenthesisExpression on line 118? Do we not call ExtractSegmentIdentifierAndParenthesisExpression before calling TryHanbdleAsKeySegment? #Closed
There was a problem hiding this comment.
This is the remains from a previous iteration. In my previous logic, I had permitted empty segment text. This is not needed now.
In reply to: 366148775 [](ancestors = 366148775)
| // If nothing to bind for key then bind the escape function directly. | ||
| if (segmentText.Length == 0) | ||
| { | ||
| return this.TryBindEscapeFunction(); |
There was a problem hiding this comment.
You have already done a somewhat expensive operation to figure out what the escape function is. You shouldn't have to redo that work when you call TryBindEscapeFunction. Can you split TryBindEscapeFunction into two pieces, the first the determines the function and the second that takes that information and does the actual binding, and call the second function from here and line 403? #Closed
There was a problem hiding this comment.
Instead of splitting it into two functions I decided to have this take in an optional function.
In reply to: 366150654 [](ancestors = 366150654)
There was a problem hiding this comment.
See my comments in iteration 22. I still think we can do a little refactoring to make the code cleaner and more performant.
In reply to: 366662603 [](ancestors = 366662603,366150654)
| // If nothing to bind for key then bind the escape function directly. | ||
| if (segmentText.Length == 0) | ||
| { | ||
| return this.TryBindEscapeFunction(); |
There was a problem hiding this comment.
return [](start = 24, length = 6)
Logic is a bit confusing, since you aren't handling it as a key, but you are handling the segment. Maybe add to the comment "and return true so that the caller knows the segment has been handled."
Also, don't you know at this point that TryBindEscapeFunction will return true (since you already verified the escape function? If, for some reason, TryBindEscapeFunction returned false, would you want this method to return false or would you want the logic below to run? (and determine if the single ":" character was to be treated as a key).
#Closed
There was a problem hiding this comment.
It can throw an error even if there is an escape function bound to this type. It will never return false as of now. More details in comments, let me know how you would like me to proceed.
I changed the code as I thought was the best possible way to handle it.
PS: I dislike the int passed to tell how many segments to remove from the list.
In reply to: 366150964 [](ancestors = 366150964)
|
|
||
| if (bindingType == null) // escape function is only for bind function | ||
|
|
||
| { |
There was a problem hiding this comment.
remove blank line #Closed
| return function.FullName(); | ||
| parenthesisExpression = function.Parameters.ElementAt(1).Name + "='" + (isComposableRequired ? identifier.Substring(0, identifier.Length - 1) : identifier.Substring(0)) + "'"; | ||
| qualifiedName = function.FullName(); | ||
|
|
There was a problem hiding this comment.
remove space at end of line. #Closed
| return false; | ||
| // should be an exception instead. | ||
| //throw ExceptionUtil.CreateBadRequestError("Bound function is not composable."); | ||
| } |
There was a problem hiding this comment.
is this work to be done? #Closed
| { | ||
| identifier = identifier.Substring(0, identifier.Length - 1); | ||
| tryBindingEscapeFunction = true; | ||
| } |
There was a problem hiding this comment.
same logic appears multiple times -- perhaps encapsulate in a common function? #Closed
| this.TryBindKeyFromParentheses(parenthesisExpression); | ||
|
|
||
| if (tryBindingEscapeFunction && !this.TryBindEscapeFunction()) | ||
| { |
There was a problem hiding this comment.
So the logic is, for each segment that could be followed by an escape function, if the identifier ends in ':', try binding the identifier up to the colon as that segment type, and see if the you can bind a function to it. If not, revert binding the segment and continue. Is that Right?
However, if the identifier ends in colon, and the segment successfully binds, then if there is no bound escape function then the only possibility is that the segment may be a key. Right? Does that simplify the subsequent logic? #Closed
There was a problem hiding this comment.
Yes, you understanding is correct regarding what I am trying to do here. However, the segment may successfully bind as type which may not have escape function bound but the segment can then be bound to a type which may have an escape function. Therefore, it is incorrect to say that it will only bind to the key segment but yes, it is very likely that it will bind to the key segment.
Even if I take that assumption, the code will be simplified but not to a great extent.
In reply to: 366156392 [](ancestors = 366156392)
Need to add code (and test) for a type cast segment followed by an escape. i.e., Orders/NS.Order:/xyz/abc: and /Customers(2)/NS.Customer:/xyz/abc: #Closed Refers to: src/Microsoft.OData.Core/UriParser/Parsers/ODataPathParser.cs:1251 in 267863a. [](commit_id = 267863a, deletion_comment = False) |
|
|
||
| this.TryBindKeySegmentIfNoResolvedParametersAndParenthesisValueExists(parenthesisExpression, returnType, resolvedParameters, segment); | ||
|
|
||
| if (tryBindingEscapeFunction && !this.TryBindEscapeFunction()) |
There was a problem hiding this comment.
!this.TryBindEscapeFunction() [](start = 44, length = 29)
shouldn't this return false if bindescapefunction returns false, so the segment can be evaluated as a key? i.e., "/Customers(2)/NS.FindOrderComposable(orderName='name'):/xyz/abc:" currently succeeds, ignoring everything after Customers(2), and returns a path with 2 segments (Customers entity set and key) if there is no escape function defined that takes an order. #Resolved
This needs to handle a function import followed by an escape function. #Closed Refers to: src/Microsoft.OData.Core/UriParser/Parsers/ODataPathParser.cs:1018 in 267863a. [](commit_id = 267863a, deletion_comment = False) |
|
|
||
| EdmModel model = new EdmModel(); | ||
|
|
||
| EdmEntityType orderType = new EdmEntityType("NS", "Order"); | ||
| orderType.AddKeys(orderType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); |
There was a problem hiding this comment.
Int32 [](start = 89, length = 5)
why do we have to change it? #Resolved
| throw ExceptionUtil.CreateSyntaxError(); | ||
| } | ||
|
|
||
|
|
There was a problem hiding this comment.
Remove Blank Space #Resolved
| private bool TryCreateEscapeFunctionSegment(string segmentText) | ||
| { | ||
| int numberOfSegmentsParsed = this.parsedSegments.Count; | ||
| string newSegmentText = segmentText.Substring(0, segmentText.Length - 1); |
There was a problem hiding this comment.
string newSegmentText = segmentText.Substring(0, segmentText.Length - 1); [](start = 12, length = 73)
this line is abrupt. Maybe add a comment or a Debug.Assert(...) to make sure the input segment text has a ":" at the end? #Closed
|
|
||
| private bool TryCreateEscapeFunctionSegment(string segmentText) | ||
| { | ||
| int numberOfSegmentsParsed = this.parsedSegments.Count; |
There was a problem hiding this comment.
int numberOfSegmentsParsed = this.parsedSegments.Count; [](start = 12, length = 55)
line 1274 can move to Line 1290, right? #Closed
There was a problem hiding this comment.
Nope. BindSegmentForEscapeFunction changes the list of segments and it's count.
In reply to: 369798577 [](ancestors = 369798577)
|
|
||
| parenthesisExpression = function.Parameters.ElementAt(1).Name + "='" + (isComposableRequired ? identifier.Substring(1, identifier.Length - 2) : identifier.Substring(1)) + "'"; | ||
| return function.FullName(); | ||
| parenthesisExpression = function.Parameters.ElementAt(1).Name + "='" + (isComposableRequired ? identifier.Substring(0, identifier.Length - 1) : identifier.Substring(0)) + "'"; |
There was a problem hiding this comment.
identifier.Substring(0) [](start = 156, length = 23)
isn't this just "identifier"? #Closed
|
|
||
|
/// |
||
| /// The text of the segment. | ||
| private bool BindSegmentForEscapeFunction(string segmentText) |
There was a problem hiding this comment.
BindSegmentForEscapeFunction [](start = 21, length = 28)
maybe name it as "BindEscapeFunctionSegment", follow up the others #Resolved
| if (this.TryCreateSegmentForOperationImport(identifier, parenthesisExpression)) | ||
| { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
here looks a lot of codes duplicated , should be just call "CreateFirstSegment"... and in else to call "CreateNexteSegment"? #Resolved
There was a problem hiding this comment.
That is what the previous iteration was doing. It added a lot of checks to existing cases. I did not want to degrade existing performance in the hot path.
In reply to: 369804646 [](ancestors = 369804646)
|
|
||
| this.parsedSegments.Add(segment); | ||
|
|
||
|
I have concerns about the duplicated codes in BindSegmentForEscapeFunction. It will double the cost to maintain. Others look good to me. So i approved it. Thanks. #ByDesign |
| /// The name of the segment | ||
| /// The query portion | ||
| /// Type to which the operation is bound. It is an optional parameter that can be evalulated from the previous segment if it is null. | ||
|
/// |
There was a problem hiding this comment.
In the case of the escape function, you actually know more than the binding type -- you know the actual specific function overload that you want to bind, parameters to pass, etc. Rather than passing in (possibly null) values to this method, I was suggesting that you factor this method into two. The first part of this method has the current signature (without the added bindingType), and it determines the binding type and specific function (i.e., everything down to 1143). And then this method calls a new method (also called by TryBindEscapeFunction) that takes the previous segment, resolved function, and resolved parameters and does the bind. #Closed
| string newIdentifier, newParenthesisExpression; | ||
| bool anotherEscapeFunctionStarts = false; | ||
|
|
||
| if (this.TryResolveEscapeFunction(bindingType, configuration.Model, out newIdentifier, out newParenthesisExpression, out anotherEscapeFunctionStarts)) |
There was a problem hiding this comment.
this.configuration.Model, to be clear where configuration is coming from? Note that you don't really have to pass this, since TryResolveEscapeFunction is an instance method and has access to the model through the same configuration property. #Resolved
|
|
||
| private static string ResolveEscapeFunction(string identifier, IEdmType bindingType, IEdmModel model, out string parenthesisExpression) | ||
| private bool TryResolveEscapeFunction(IEdmType bindingType, IEdmModel model, out string qualifiedName, out string parenthesisExpression, out bool anotherEscapeFunctionStarts) | ||
| { |
There was a problem hiding this comment.
Note that this function was originally written to be called within TryCreateSegmentForOperation, so its inputs/outputs may be a little different now that you are calling this before trying to bind the operation (you now have the actual function you need to bind). So think about what input/output parameters make sense in this new usage, and whether it should be called from TryBindEscapeFunction or the logic should be merged with TryBindEscapeFunction. #Closed
|
|
||
|
|
||
| ODataPathSegment previous = this.parsedSegments[this.parsedSegments.Count - 1]; | ||
| // $value |
There was a problem hiding this comment.
Why move this? #Closed
There was a problem hiding this comment.
You mean why move the TryCreateEscapeFunctionSegment? I did that to not ExtractIdentifierAndParenthesis proactively for escape function segment. If it is successful, we may not need to at this point.
In reply to: 369818816 [](ancestors = 369818816)
There was a problem hiding this comment.
See iteration 22: you had moved ODataPathSegment previous = this.parsedSegments[this.parsedSegments.Count - 1]; from line 1202 (immediately before using it to check previous.TargetKind == RequestTargetKind.Primitive, where is is first used) to line 1195 (before TryCreateValueSegment). It looks like it moved back in iteration 23.
In reply to: 369827716 [](ancestors = 369827716,369818816)
| } | ||
|
|
||
| private bool TryCreateEscapeFunctionSegment(string segmentText) | ||
| { |
There was a problem hiding this comment.
assert that segmentText ends in ":"? #Closed
| anotherEscapeFunctionStarts = true; | ||
| } | ||
|
|
||
| bool isComposableRequired = identifier.Length >= 1 && identifier[identifier.Length - 1] == ':'; |
There was a problem hiding this comment.
identifier.Length [](start = 40, length = 17)
add check for identifier != null. #Resolved
|
|
||
| bool isComposableRequired = identifier.Length >= 1 && identifier[identifier.Length - 1] == ':'; | ||
|
|
||
| if ((function.Parameters.FirstOrDefault().Type.Definition != bindingType) || (isComposableRequired && !function.IsComposable) || (!isComposableRequired && function.IsComposable)) |
There was a problem hiding this comment.
add comment describing logic. #Resolved
if function == null (for example, because there were escape functions that matched the binding parameter, but they didn't meet the composability requirements), shouldn't we return false, rather than throw here? Refers to: src/Microsoft.OData.Core/UriParser/Parsers/ODataPathParser.cs:1858 in da28656. [](commit_id = da28656, deletion_comment = False) |
| @@ -1621,8 +1862,10 @@ private static string ResolveEscapeFunction(string identifier, IEdmType bindingT | |||
| throw ExceptionUtil.CreateBadRequestError(ODataErrorStrings.RequestUriProcessor_EscapeFunctionMustHaveOneStringParameter(function.FullName())); | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
isn't this already validated in IsUrlEscapeFunction? If so, maybe just assert here? #Closed
There was a problem hiding this comment.
Nope, IsUrlEscapeFunction just returns the functions based on the annotation. Although it is unlikely that this will ever get thrown if people annotate their escape functions carefully.
In reply to: 369831446 [](ancestors = 369831446)
| if (function == null) | ||
| { | ||
|
function = model.FindBoundOperations(bindingType).OfType |
||
| } |
There was a problem hiding this comment.
Yikes! This is the third time we've called a fairly expensive FindBoundOperations and run a fairly expensive (and similar) LINQ query over the results. Any way we can optimize this? Maybe first determine the list of candidates:
List candidates = model.FindBoundOperations(bindingType).OfType().Where(f=>IsUrlEscapeFunction(model, f)).ToList();
and then pick the best from that (presumably rather small) list?
-exact match of bindingType and composability
-any (base) type and composability
-return false? (I think we decided composability had to exactly match?) #Closed
There was a problem hiding this comment.
|
I agree. Can't think of a better solution though. In reply to: 577390017 [](ancestors = 577390017) |
|
|
||
| return true; | ||
|
|
||
| } |
There was a problem hiding this comment.
nit: remove blank line #Closed
|
private static IEdmFunction FindBestMatchForEscapeFunction(IEnumerable |
||
| { | ||
| IEdmFunction bestCandidate = null; | ||
| foreach (IEdmFunction f in candidtates) |
There was a problem hiding this comment.
candidtates [](start = 39, length = 11)
nit: spelling #Resolved
| continue; | ||
| } | ||
|
|
||
| if (f.Parameters != null && f.Parameters.Count() == 2 && f.Parameters.ElementAt(1).Type.IsString()) |
There was a problem hiding this comment.
Parameters.Count() [](start = 46, length = 18)
nit: rather than repeatedly re-enumerating, probably more efficient to do ToList; then you can index for the first and second parameters. #Resolved
| } | ||
| else | ||
| { | ||
| bestCandidate = f; |
There was a problem hiding this comment.
You are resetting the bestCandidate each time, which means that, if there is not an exact match, you'll pick the last one that matches (which, I think, is likely to be the base type, rather than the most derived type). You could instead check to see if the current candidate has the current bestCandidate (if any) as a base type (recursively) and, if so, make the current candidate your new bestCandidate. #Closed
279f0e7 to
bcfec25
Compare
Issues
This pull request fixes the escape function uri.
Description
The previous implementation of escape function uri was not able to distinguish if a request intended to use an escape function or a key-value began with a ':' when the key is expressed as a segment. Therefore, the parsing would fail for the requests with key as segment with value starting with ':' with an error that no escape function was found for the type.
Now, we change how we parse segments and give precedence to an escape function if one exists for the type otherwise treat it as a key-value.
This pull requests
Additional work necessary
Release notes need to mention these changes when merged.