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 #2794: Enable $root path #3157

Open
wants to merge 1 commit into
base: main
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
1 change: 1 addition & 0 deletions src/Microsoft.OData.Client/Microsoft.OData.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
<Compile Include="..\Microsoft.OData.Core\UriParser\SyntacticAst\SelectToken.cs" Link="ALinq\UriParser\SyntacticAst\SelectToken.cs" />
<Compile Include="..\Microsoft.OData.Core\UriParser\SyntacticAst\StarToken.cs" Link="ALinq\UriParser\SyntacticAst\StarToken.cs" />
<Compile Include="..\Microsoft.OData.Core\UriParser\SyntacticAst\SystemToken.cs" Link="ALinq\UriParser\SyntacticAst\SystemToken.cs" />
<Compile Include="..\Microsoft.OData.Core\UriParser\SyntacticAst\RootPathToken.cs" Link="ALinq\UriParser\SyntacticAst\RootPathToken.cs" />
<Compile Include="..\Microsoft.OData.Core\UriParser\SyntacticAst\UnaryOperatorToken.cs" Link="ALinq\UriParser\SyntacticAst\UnaryOperatorToken.cs" />
<Compile Include="..\Microsoft.OData.Core\UriParser\Aggregation\ApplyTransformationToken.cs" Link="ALinq\UriParser\Aggregation\ApplyTransformationToken.cs" />
<Compile Include="..\Microsoft.OData.Core\UriParser\Aggregation\EntitySetAggregateToken.cs" Link="ALinq\UriParser\Aggregation\EntitySetAggregateToken.cs" />
Expand Down
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
14 changes: 14 additions & 0 deletions src/Microsoft.OData.Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt
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
41 changes: 41 additions & 0 deletions src/Microsoft.OData.Core/UriParser/Binders/MetadataBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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.");
Copy link
Preview

Copilot AI Dec 23, 2024

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

Suggested change
throw new ODataException("Empty root path is not valid.");
throw new ODataException("An empty root path is not valid.");

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options
Copy link
Contributor

Choose a reason for hiding this comment

The 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.");
Copy link
Contributor

Choose a reason for hiding this comment

The 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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand Up @@ -1064,6 +1064,12 @@ private QueryToken ParsePrimary()
expr = this.ParsePrimaryStart();
}

// Pase the $root path, for example: '$root/people(12)'
Copy link
Preview

Copilot AI Dec 23, 2024

Choose a reason for hiding this comment

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

The comment contains a typo: 'Pase' should be 'Parse'.

Suggested change
// Pase the $root path, for example: '$root/people(12)'
// Parse the $root path, for example: '$root/people(12)'

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options
if (expr != null && expr.Kind == QueryTokenKind.RootPath)
{
return ParseRootPath((RootPathToken)expr);
}

while (this.lexer.CurrentToken.Kind == ExpressionTokenKind.Slash)
{
this.lexer.NextToken();
Expand Down Expand Up @@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The 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>
Expand Down Expand Up @@ -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");
Copy link
Contributor

Choose a reason for hiding this comment

The 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);
Expand Down
56 changes: 56 additions & 0 deletions src/Microsoft.OData.Core/UriParser/SemanticAst/RootPathNode.cs
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
Expand Up @@ -159,6 +159,11 @@ public enum QueryTokenKind
/// <summary>
/// $count segment
/// </summary>
CountSegment = 32
CountSegment = 32,

/// <summary>
/// $root path
/// </summary>
RootPath = 33
}
}
40 changes: 40 additions & 0 deletions src/Microsoft.OData.Core/UriParser/SyntacticAst/RootPathToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//---------------------------------------------------------------------
// <copyright file="EndPathToken.cs" company="Microsoft">
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be RootPathToken.cs instead of EndPathToken.cs

Suggested change
// <copyright file="EndPathToken.cs" company="Microsoft">
// <copyright file="RootPathToken.cs" company="Microsoft">

// 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);
}
}
}
10 changes: 10 additions & 0 deletions src/Microsoft.OData.Core/UriParser/TreeNodeKinds/QueryNodeKind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ public enum QueryNodeKind
/// Node that represents a collection of constants.
/// </summary>
CollectionConstant = InternalQueryNodeKind.CollectionConstant,

/// <summary>
/// Node that represents a $root path
/// </summary>
RootPath = InternalQueryNodeKind.RootPath,
}

/// <summary>
Expand Down Expand Up @@ -359,5 +364,10 @@ internal enum InternalQueryNodeKind
/// Node that represents a collection of constants.
/// </summary>
CollectionConstant = 33,

/// <summary>
/// Node that represetns a $root path
/// </summary>
RootPath,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,5 +192,12 @@ public interface ISyntacticTreeVisitor<T>
/// <param name="tokenIn">The GroupByToken to bind</param>
/// <returns>A T node bound to this GroupByToken</returns>
T Visit(GroupByToken tokenIn);

/// <summary>
/// Visits a RootPathToken
/// </summary>
/// <param name="tokenIn">The RootPathToken to bind</param>
/// <returns>A user defined value</returns>
T Visit(RootPathToken tokenIn);
}
}
10 changes: 10 additions & 0 deletions src/Microsoft.OData.Core/UriParser/Visitors/QueryNodeVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -313,5 +313,15 @@ public virtual T Visit(InNode nodeIn)
{
throw new NotImplementedException();
}

/// <summary>
/// Visit an RootPathNode
/// </summary>
/// <param name="nodeIn">the node to visit</param>
/// <returns>Defined by the implementer</returns>
public virtual T Visit(RootPathNode nodeIn)
{
throw new NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -288,5 +288,15 @@ public virtual T Visit(ComputeExpressionToken tokenIn)
{
throw new NotImplementedException();
}

/// <summary>
/// Visits a RootPathToken
/// </summary>
/// <param name="tokenIn">The RootPathToken to bind</param>
/// <returns>A user defined value</returns>
public virtual T Visit(RootPathToken tokenIn)
{
throw new NotImplementedException();
}
}
}
Loading