-
Notifications
You must be signed in to change notification settings - Fork 352
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 #2794: Enable $root path #3157
base: main
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 |
---|---|---|
@@ -0,0 +1,7 @@ | ||
Microsoft.OData.Client.ALinq.UriParser.ISyntacticTreeVisitor<T>.Visit(Microsoft.OData.Client.ALinq.UriParser.RootPathToken tokenIn) -> T | ||
Microsoft.OData.Client.ALinq.UriParser.QueryTokenKind.RootPath = 33 -> Microsoft.OData.Client.ALinq.UriParser.QueryTokenKind | ||
Microsoft.OData.Client.ALinq.UriParser.RootPathToken | ||
Microsoft.OData.Client.ALinq.UriParser.RootPathToken.RootPathToken() -> void | ||
Microsoft.OData.Client.ALinq.UriParser.RootPathToken.Segments.get -> System.Collections.Generic.IList<string> | ||
override Microsoft.OData.Client.ALinq.UriParser.RootPathToken.Accept<T>(Microsoft.OData.Client.ALinq.UriParser.ISyntacticTreeVisitor<T> visitor) -> T | ||
override Microsoft.OData.Client.ALinq.UriParser.RootPathToken.Kind.get -> Microsoft.OData.Client.ALinq.UriParser.QueryTokenKind |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
Microsoft.OData.UriParser.ISyntacticTreeVisitor<T>.Visit(Microsoft.OData.UriParser.RootPathToken tokenIn) -> T | ||
Microsoft.OData.UriParser.QueryNodeKind.RootPath = 34 -> Microsoft.OData.UriParser.QueryNodeKind | ||
Microsoft.OData.UriParser.QueryTokenKind.RootPath = 33 -> Microsoft.OData.UriParser.QueryTokenKind | ||
Microsoft.OData.UriParser.RootPathNode | ||
Microsoft.OData.UriParser.RootPathNode.Path.get -> Microsoft.OData.UriParser.ODataPath | ||
Microsoft.OData.UriParser.RootPathNode.RootPathNode(Microsoft.OData.UriParser.ODataPath path, Microsoft.OData.Edm.IEdmTypeReference typeRef) -> void | ||
Microsoft.OData.UriParser.RootPathToken | ||
Microsoft.OData.UriParser.RootPathToken.RootPathToken() -> void | ||
Microsoft.OData.UriParser.RootPathToken.Segments.get -> System.Collections.Generic.IList<string> | ||
override Microsoft.OData.UriParser.RootPathNode.Accept<T>(Microsoft.OData.UriParser.QueryNodeVisitor<T> visitor) -> T | ||
override Microsoft.OData.UriParser.RootPathNode.TypeReference.get -> Microsoft.OData.Edm.IEdmTypeReference | ||
override Microsoft.OData.UriParser.RootPathToken.Accept<T>(Microsoft.OData.UriParser.ISyntacticTreeVisitor<T> visitor) -> T | ||
override Microsoft.OData.UriParser.RootPathToken.Kind.get -> Microsoft.OData.UriParser.QueryTokenKind | ||
virtual Microsoft.OData.UriParser.QueryNodeVisitor<T>.Visit(Microsoft.OData.UriParser.RootPathNode nodeIn) -> T |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,8 @@ namespace Microsoft.OData.UriParser | |
using System.Collections.Generic; | ||
using System.Diagnostics.CodeAnalysis; | ||
using Microsoft.OData.Core; | ||
using Microsoft.OData.Edm; | ||
using Microsoft.OData.Metadata; | ||
|
||
#endregion Namespaces | ||
|
||
|
@@ -201,6 +203,9 @@ protected internal QueryNode Bind(QueryToken token) | |
case QueryTokenKind.CountSegment: | ||
result = this.BindCountSegment((CountSegmentToken)token); | ||
break; | ||
case QueryTokenKind.RootPath: | ||
result = this.BindRootPath((RootPathToken)token); | ||
break; | ||
default: | ||
throw new ODataException(Error.Format(SRResources.MetadataBinder_UnsupportedQueryTokenKind, token.Kind)); | ||
} | ||
|
@@ -384,5 +389,41 @@ protected virtual QueryNode BindCountSegment(CountSegmentToken countSegmentToken | |
CountSegmentBinder countSegmentBinder = new CountSegmentBinder(this.Bind, this.BindingState); | ||
return countSegmentBinder.BindCountSegment(countSegmentToken); | ||
} | ||
|
||
/// <summary> | ||
/// Binds a RootPathToken. | ||
/// </summary> | ||
/// <param name="rootPathToken">The RootPath token to bind.</param> | ||
/// <returns>The bound RootPath token.</returns> | ||
protected virtual QueryNode BindRootPath(RootPathToken rootPathToken) | ||
{ | ||
ODataPath path = ODataPathFactory.BindPath(rootPathToken.Segments, this.BindingState.Configuration); | ||
|
||
ODataPathSegment lastSegment = path.LastSegment; | ||
if (lastSegment == null) | ||
{ | ||
throw new ODataException("Empty root path is not valid."); | ||
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. You can move this string to SRResources |
||
} | ||
|
||
// The $root literal can be used in expressions to refer to resources of the same service. It can be used as a single-valued expression or within complex or collection literals. | ||
if (!lastSegment.SingleResult) | ||
{ | ||
throw new ODataException("The $root literal can be used in expressions to refer to resources of the same service. It can be used as a single-valued expression or within complex or collection literals."); | ||
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. Same apply here. Move this string to SRResources |
||
} | ||
|
||
// The EdmType could be null for Dynamic path segment | ||
IEdmTypeReference typeReference = lastSegment.EdmType == null ? null : lastSegment.EdmType.ToTypeReference(true); | ||
return new RootPathNode(path, typeReference); | ||
|
||
// Keep them for discussion, will remove after discussion. | ||
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. Do you want to create an issue on Github to start the discussion around this. |
||
//// If it's collection segment, the typeReference should not be null and it should be the collection type reference | ||
//IEdmCollectionTypeReference collectionTypeRef = typeReference as IEdmCollectionTypeReference; | ||
//if (collectionTypeRef == null) | ||
//{ | ||
// throw new ODataException("Empty root path is not valid."); | ||
//} | ||
|
||
//return new RootPathCollectionValueNode(path, collectionTypeRef); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -1064,6 +1064,12 @@ private QueryToken ParsePrimary() | |||||
expr = this.ParsePrimaryStart(); | ||||||
} | ||||||
|
||||||
// Pase the $root path, for example: '$root/people(12)' | ||||||
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. The comment contains a typo: 'Pase' should be 'Parse'.
Suggested change
Copilot is powered by AI, so mistakes are possible. Review output carefully before use. |
||||||
if (expr != null && expr.Kind == QueryTokenKind.RootPath) | ||||||
{ | ||||||
return ParseRootPath((RootPathToken)expr); | ||||||
} | ||||||
|
||||||
while (this.lexer.CurrentToken.Kind == ExpressionTokenKind.Slash) | ||||||
{ | ||||||
this.lexer.NextToken(); | ||||||
|
@@ -1138,6 +1144,66 @@ private QueryToken ParsePrimaryStart() | |||||
} | ||||||
} | ||||||
|
||||||
private RootPathToken ParseRootPath(RootPathToken rootPathToken) | ||||||
{ | ||||||
while (this.lexer.CurrentToken.Kind == ExpressionTokenKind.Slash) | ||||||
{ | ||||||
this.lexer.NextToken(); // read over the '/' | ||||||
|
||||||
// a function call or an entity with key value is treated same as function call. | ||||||
bool identifierIsFunction = this.lexer.ExpandIdentifierAsFunction(); | ||||||
if (identifierIsFunction && TryParseIdentifierAsFunction(lexer, out string result)) | ||||||
{ | ||||||
rootPathToken.Segments.Add(result); | ||||||
this.lexer.NextToken(); | ||||||
continue; | ||||||
} | ||||||
|
||||||
if (this.lexer.PeekNextToken().Kind == ExpressionTokenKind.Dot) | ||||||
{ | ||||||
ReadOnlySpan<char> dotIdentifier = this.lexer.ReadDottedIdentifier(false); | ||||||
rootPathToken.Segments.Add(dotIdentifier.ToString()); | ||||||
//lexer.NextToken(); | ||||||
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 this be uncommented or you can add a comment on the top to explain why it is commented out. |
||||||
continue; | ||||||
} | ||||||
|
||||||
//lexer.ValidateToken(ExpressionTokenKind.Identifier); // could be integerLiteral or others, for example: /people/1234 | ||||||
rootPathToken.Segments.Add(this.lexer.CurrentToken.Text.ToString()); | ||||||
this.lexer.NextToken(); | ||||||
} | ||||||
|
||||||
return rootPathToken; | ||||||
} | ||||||
|
||||||
private static bool TryParseIdentifierAsFunction(ExpressionLexer lexer, out string result) | ||||||
{ | ||||||
result = null; | ||||||
ReadOnlySpan<char> functionName; | ||||||
|
||||||
ExpressionLexer.ExpressionLexerPosition position = lexer.SnapshotPosition(); | ||||||
|
||||||
if (lexer.PeekNextToken().Kind == ExpressionTokenKind.Dot) | ||||||
{ | ||||||
// handle the case where we prefix a function with its namespace. | ||||||
functionName = lexer.ReadDottedIdentifier(false); | ||||||
} | ||||||
else | ||||||
{ | ||||||
functionName = lexer.CurrentToken.Span; | ||||||
lexer.NextToken(); | ||||||
} | ||||||
|
||||||
if (lexer.CurrentToken.Kind != ExpressionTokenKind.OpenParen) | ||||||
{ | ||||||
lexer.RestorePosition(position); | ||||||
return false; | ||||||
} | ||||||
|
||||||
string parameters = lexer.AdvanceThroughBalancedParentheticalExpression(); | ||||||
result = $"{functionName}{parameters}"; | ||||||
return true; | ||||||
} | ||||||
|
||||||
/// <summary> | ||||||
/// Parses parenthesized expressions. | ||||||
/// </summary> | ||||||
|
@@ -1250,6 +1316,17 @@ private QueryToken ParseSegment(QueryToken parent) | |||||
{ | ||||||
string propertyName = this.lexer.CurrentToken.GetIdentifier().ToString(); | ||||||
this.lexer.NextToken(); | ||||||
|
||||||
if (string.Equals(propertyName, "$root", enableCaseInsensitiveBuiltinIdentifier ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) | ||||||
{ | ||||||
if (parent != null) | ||||||
{ | ||||||
throw new ODataException("$root should be the top-level segment"); | ||||||
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. Same apply here. |
||||||
} | ||||||
|
||||||
return new RootPathToken(); | ||||||
} | ||||||
|
||||||
if (this.parameters.Contains(propertyName) && parent == null) | ||||||
{ | ||||||
return new RangeVariableToken(propertyName); | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
//--------------------------------------------------------------------- | ||
// <copyright file="RootPathNode.cs" company="Microsoft"> | ||
// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. | ||
// </copyright> | ||
//--------------------------------------------------------------------- | ||
|
||
namespace Microsoft.OData.UriParser | ||
{ | ||
using Microsoft.OData.Edm; | ||
|
||
/// <summary> | ||
/// The $root literal can be used in expressions to refer to resources of the same service. It can be used as a single-valued expression or within complex or collection literals. | ||
/// </summary> | ||
public sealed class RootPathNode : SingleValueNode | ||
{ | ||
/// <summary> | ||
/// Created a RootPathNode with the given path and the given type. | ||
/// </summary> | ||
/// <param name="path">The OData path.</param> | ||
/// <param name="typeRef">The path type. It could be null if the last segment is dynamic.</param> | ||
public RootPathNode(ODataPath path, IEdmTypeReference typeRef) | ||
{ | ||
ExceptionUtils.CheckArgumentNotNull(path, "path"); | ||
Path = path; | ||
TypeReference = typeRef; | ||
} | ||
|
||
/// <summary> | ||
/// Gets the OData path. | ||
/// </summary> | ||
public ODataPath Path { get;} | ||
|
||
/// <summary> | ||
/// Gets the type of the single value this node represents. | ||
/// </summary> | ||
public override IEdmTypeReference TypeReference { get; } | ||
|
||
/// <summary> | ||
/// Gets the kind of this node. | ||
/// </summary> | ||
internal override InternalQueryNodeKind InternalKind => InternalQueryNodeKind.RootPath; | ||
|
||
/// <summary> | ||
/// Accept a <see cref="QueryNodeVisitor{T}"/> to walk a tree of <see cref="QueryNode"/>s. | ||
/// </summary> | ||
/// <typeparam name="T">Type that the visitor will return after visiting this token.</typeparam> | ||
/// <param name="visitor">An implementation of the visitor interface.</param> | ||
/// <returns>An object whose type is determined by the type parameter of the visitor.</returns> | ||
/// <exception cref="System.ArgumentNullException">Throws if the input visitor is null.</exception> | ||
public override T Accept<T>(QueryNodeVisitor<T> visitor) | ||
{ | ||
ExceptionUtils.CheckArgumentNotNull(visitor, "visitor"); | ||
return visitor.Visit(this); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,40 @@ | ||||||||
//--------------------------------------------------------------------- | ||||||||
// <copyright file="EndPathToken.cs" company="Microsoft"> | ||||||||
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 this be
Suggested change
|
||||||||
// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. | ||||||||
// </copyright> | ||||||||
//--------------------------------------------------------------------- | ||||||||
|
||||||||
using System.Collections.Generic; | ||||||||
#if ODATA_CLIENT | ||||||||
namespace Microsoft.OData.Client.ALinq.UriParser | ||||||||
#else | ||||||||
namespace Microsoft.OData.UriParser | ||||||||
#endif | ||||||||
{ | ||||||||
/// <summary> | ||||||||
/// Lexical token representing a $root path. | ||||||||
/// </summary> | ||||||||
public sealed class RootPathToken : QueryToken | ||||||||
{ | ||||||||
/// <summary> | ||||||||
/// The kind of the query token. | ||||||||
/// </summary> | ||||||||
public override QueryTokenKind Kind => QueryTokenKind.RootPath; | ||||||||
|
||||||||
/// <summary> | ||||||||
/// Path segments (without the $root leading segment) | ||||||||
/// </summary> | ||||||||
public IList<string> Segments { get; } = new List<string>(); | ||||||||
|
||||||||
/// <summary> | ||||||||
/// Accept a <see cref="ISyntacticTreeVisitor{T}"/> to walk a tree of <see cref="QueryToken"/>s. | ||||||||
/// </summary> | ||||||||
/// <typeparam name="T">Type that the visitor will return after visiting this token.</typeparam> | ||||||||
/// <param name="visitor">An implementation of the visitor interface.</param> | ||||||||
/// <returns>An object whose type is determined by the type parameter of the visitor.</returns> | ||||||||
public override T Accept<T>(ISyntacticTreeVisitor<T> visitor) | ||||||||
{ | ||||||||
return visitor.Visit(this); | ||||||||
} | ||||||||
} | ||||||||
} |
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.
The error message should be 'An empty root path is not valid.'
Copilot is powered by AI, so mistakes are possible. Review output carefully before use.