Skip to content

Commit 74859bc

Browse files
committed
Track position in query string parsing and report location on failure
1 parent a5794c3 commit 74859bc

File tree

46 files changed

+879
-525
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+879
-525
lines changed

src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ public class PaginationElementQueryStringValueExpression : QueryExpression
1010
{
1111
public ResourceFieldChainExpression? Scope { get; }
1212
public int Value { get; }
13+
public int Position { get; }
1314

14-
public PaginationElementQueryStringValueExpression(ResourceFieldChainExpression? scope, int value)
15+
public PaginationElementQueryStringValueExpression(ResourceFieldChainExpression? scope, int value, int position)
1516
{
1617
Scope = scope;
1718
Value = value;
19+
Position = position;
1820
}
1921

2022
public override TResult Accept<TArgument, TResult>(QueryExpressionVisitor<TArgument, TResult> visitor, TArgument argument)
@@ -24,12 +26,12 @@ public override TResult Accept<TArgument, TResult>(QueryExpressionVisitor<TArgum
2426

2527
public override string ToString()
2628
{
27-
return Scope == null ? Value.ToString() : $"{Scope}: {Value}";
29+
return Scope == null ? $"{Value} at {Position}" : $"{Scope}: {Value} at {Position}";
2830
}
2931

3032
public override string ToFullString()
3133
{
32-
return Scope == null ? Value.ToString() : $"{Scope.ToFullString()}: {Value}";
34+
return Scope == null ? $"{Value} at {Position}" : $"{Scope.ToFullString()}: {Value} at {Position}";
3335
}
3436

3537
public override bool Equals(object? obj)
@@ -46,11 +48,11 @@ public override bool Equals(object? obj)
4648

4749
var other = (PaginationElementQueryStringValueExpression)obj;
4850

49-
return Equals(Scope, other.Scope) && Value == other.Value;
51+
return Equals(Scope, other.Scope) && Value == other.Value && Position == other.Position;
5052
}
5153

5254
public override int GetHashCode()
5355
{
54-
return HashCode.Combine(Scope, Value);
56+
return HashCode.Combine(Scope, Value, Position);
5557
}
5658
}

src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ public override QueryExpression PaginationElementQueryStringValue(PaginationElem
240240
{
241241
ResourceFieldChainExpression? newScope = expression.Scope != null ? Visit(expression.Scope, argument) as ResourceFieldChainExpression : null;
242242

243-
var newExpression = new PaginationElementQueryStringValueExpression(newScope, expression.Value);
243+
var newExpression = new PaginationElementQueryStringValueExpression(newScope, expression.Value, expression.Position);
244244
return newExpression.Equals(expression) ? expression : newExpression;
245245
}
246246

src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs

Lines changed: 72 additions & 37 deletions
Large diffs are not rendered by default.

src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs

Lines changed: 34 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Collections.Immutable;
2-
using System.Text;
32
using JetBrains.Annotations;
43
using JsonApiDotNetCore.Configuration;
54
using JsonApiDotNetCore.Errors;
@@ -19,15 +18,15 @@ public IncludeExpression Parse(string source, ResourceType resourceTypeInScope,
1918

2019
Tokenize(source);
2120

22-
IncludeExpression expression = ParseInclude(resourceTypeInScope, maximumDepth);
21+
IncludeExpression expression = ParseInclude(source, resourceTypeInScope, maximumDepth);
2322

2423
AssertTokenStackIsEmpty();
25-
ValidateMaximumIncludeDepth(maximumDepth, expression);
24+
ValidateMaximumIncludeDepth(maximumDepth, expression, 0);
2625

2726
return expression;
2827
}
2928

30-
protected IncludeExpression ParseInclude(ResourceType resourceTypeInScope, int? maximumDepth)
29+
protected IncludeExpression ParseInclude(string source, ResourceType resourceTypeInScope, int? maximumDepth)
3130
{
3231
var treeRoot = IncludeTreeNode.CreateRoot(resourceTypeInScope);
3332
bool isAtStart = true;
@@ -43,13 +42,13 @@ protected IncludeExpression ParseInclude(ResourceType resourceTypeInScope, int?
4342
isAtStart = false;
4443
}
4544

46-
ParseRelationshipChain(treeRoot);
45+
ParseRelationshipChain(source, treeRoot);
4746
}
4847

4948
return treeRoot.ToExpression();
5049
}
5150

52-
private void ParseRelationshipChain(IncludeTreeNode treeRoot)
51+
private void ParseRelationshipChain(string source, IncludeTreeNode treeRoot)
5352
{
5453
// A relationship name usually matches a single relationship, even when overridden in derived types.
5554
// But in the following case, two relationships are matched on GET /shoppingBaskets?include=items:
@@ -77,27 +76,29 @@ private void ParseRelationshipChain(IncludeTreeNode treeRoot)
7776
// that there's currently no way to include Products without Articles. We could add such optional upcast syntax
7877
// in the future, if desired.
7978

80-
ICollection<IncludeTreeNode> children = ParseRelationshipName(treeRoot.AsList());
79+
ICollection<IncludeTreeNode> children = ParseRelationshipName(source, treeRoot.AsList());
8180

8281
while (TokenStack.TryPeek(out Token? nextToken) && nextToken.Kind == TokenKind.Period)
8382
{
8483
EatSingleCharacterToken(TokenKind.Period);
8584

86-
children = ParseRelationshipName(children);
85+
children = ParseRelationshipName(source, children);
8786
}
8887
}
8988

90-
private ICollection<IncludeTreeNode> ParseRelationshipName(ICollection<IncludeTreeNode> parents)
89+
private ICollection<IncludeTreeNode> ParseRelationshipName(string source, ICollection<IncludeTreeNode> parents)
9190
{
91+
int position = GetNextTokenPositionOrEnd();
92+
9293
if (TokenStack.TryPop(out Token? token) && token.Kind == TokenKind.Text)
9394
{
94-
return LookupRelationshipName(token.Value!, parents);
95+
return LookupRelationshipName(token.Value!, parents, source, position);
9596
}
9697

97-
throw new QueryParseException("Relationship name expected.");
98+
throw new QueryParseException("Relationship name expected.", position);
9899
}
99100

100-
private ICollection<IncludeTreeNode> LookupRelationshipName(string relationshipName, ICollection<IncludeTreeNode> parents)
101+
private ICollection<IncludeTreeNode> LookupRelationshipName(string relationshipName, ICollection<IncludeTreeNode> parents, string source, int position)
101102
{
102103
List<IncludeTreeNode> children = new();
103104
HashSet<RelationshipAttribute> relationshipsFound = new();
@@ -118,116 +119,94 @@ private ICollection<IncludeTreeNode> LookupRelationshipName(string relationshipN
118119
}
119120
}
120121

121-
AssertRelationshipsFound(relationshipsFound, relationshipName, parents);
122-
AssertAtLeastOneCanBeIncluded(relationshipsFound, relationshipName, parents);
122+
AssertRelationshipsFound(relationshipsFound, relationshipName, parents, position);
123+
AssertAtLeastOneCanBeIncluded(relationshipsFound, relationshipName, source, position);
123124

124125
return children;
125126
}
126127

127-
private static void AssertRelationshipsFound(ISet<RelationshipAttribute> relationshipsFound, string relationshipName, ICollection<IncludeTreeNode> parents)
128+
private static void AssertRelationshipsFound(ISet<RelationshipAttribute> relationshipsFound, string relationshipName, ICollection<IncludeTreeNode> parents,
129+
int position)
128130
{
129131
if (relationshipsFound.Any())
130132
{
131133
return;
132134
}
133135

134-
string[] parentPaths = parents.Select(parent => parent.Path).Distinct().Where(path => path != string.Empty).ToArray();
135-
string path = parentPaths.Length > 0 ? $"{parentPaths[0]}.{relationshipName}" : relationshipName;
136-
137136
ResourceType[] parentResourceTypes = parents.Select(parent => parent.Relationship.RightType).Distinct().ToArray();
138137

139138
bool hasDerivedTypes = parents.Any(parent => parent.Relationship.RightType.DirectlyDerivedTypes.Count > 0);
140139

141-
string message = ErrorFormatter.GetForNoneFound(ResourceFieldCategory.Relationship, relationshipName, path, parentResourceTypes, hasDerivedTypes);
142-
throw new QueryParseException(message);
140+
string message = ErrorFormatter.GetForNoneFound(ResourceFieldCategory.Relationship, relationshipName, parentResourceTypes, hasDerivedTypes);
141+
throw new QueryParseException(message, position);
143142
}
144143

145-
private static void AssertAtLeastOneCanBeIncluded(ISet<RelationshipAttribute> relationshipsFound, string relationshipName,
146-
ICollection<IncludeTreeNode> parents)
144+
private static void AssertAtLeastOneCanBeIncluded(ISet<RelationshipAttribute> relationshipsFound, string relationshipName, string source, int position)
147145
{
148146
if (relationshipsFound.All(relationship => relationship.IsIncludeBlocked()))
149147
{
150-
string parentPath = parents.First().Path;
151148
ResourceType resourceType = relationshipsFound.First().LeftType;
149+
string message = $"Including the relationship '{relationshipName}' on '{resourceType}' is not allowed.";
152150

153-
string message = parentPath == string.Empty
154-
? $"Including the relationship '{relationshipName}' on '{resourceType}' is not allowed."
155-
: $"Including the relationship '{relationshipName}' in '{parentPath}.{relationshipName}' on '{resourceType}' is not allowed.";
151+
var exception = new QueryParseException(message, position);
152+
string specificMessage = exception.GetMessageWithPosition(source);
156153

157-
throw new InvalidQueryStringParameterException("include", "The specified include is invalid.", message);
154+
throw new InvalidQueryStringParameterException("include", "The specified include is invalid.", specificMessage);
158155
}
159156
}
160157

161-
private static void ValidateMaximumIncludeDepth(int? maximumDepth, IncludeExpression include)
158+
private static void ValidateMaximumIncludeDepth(int? maximumDepth, IncludeExpression include, int position)
162159
{
163160
if (maximumDepth != null)
164161
{
165162
Stack<RelationshipAttribute> parentChain = new();
166163

167164
foreach (IncludeElementExpression element in include.Elements)
168165
{
169-
ThrowIfMaximumDepthExceeded(element, parentChain, maximumDepth.Value);
166+
ThrowIfMaximumDepthExceeded(element, parentChain, maximumDepth.Value, position);
170167
}
171168
}
172169
}
173170

174-
private static void ThrowIfMaximumDepthExceeded(IncludeElementExpression includeElement, Stack<RelationshipAttribute> parentChain, int maximumDepth)
171+
private static void ThrowIfMaximumDepthExceeded(IncludeElementExpression includeElement, Stack<RelationshipAttribute> parentChain, int maximumDepth,
172+
int position)
175173
{
176174
parentChain.Push(includeElement.Relationship);
177175

178176
if (parentChain.Count > maximumDepth)
179177
{
180178
string path = string.Join('.', parentChain.Reverse().Select(relationship => relationship.PublicName));
181-
throw new QueryParseException($"Including '{path}' exceeds the maximum inclusion depth of {maximumDepth}.");
179+
throw new QueryParseException($"Including '{path}' exceeds the maximum inclusion depth of {maximumDepth}.", position);
182180
}
183181

184182
foreach (IncludeElementExpression child in includeElement.Children)
185183
{
186-
ThrowIfMaximumDepthExceeded(child, parentChain, maximumDepth);
184+
ThrowIfMaximumDepthExceeded(child, parentChain, maximumDepth, position);
187185
}
188186

189187
parentChain.Pop();
190188
}
191189

192-
protected override IImmutableList<ResourceFieldAttribute> OnResolveFieldChain(string path, FieldChainRequirements chainRequirements)
190+
protected override IImmutableList<ResourceFieldAttribute> OnResolveFieldChain(string path, int position, FieldChainRequirements chainRequirements)
193191
{
194192
throw new NotSupportedException();
195193
}
196194

197195
private sealed class IncludeTreeNode
198196
{
199-
private readonly IncludeTreeNode? _parent;
200197
private readonly IDictionary<RelationshipAttribute, IncludeTreeNode> _children = new Dictionary<RelationshipAttribute, IncludeTreeNode>();
201198

202199
public RelationshipAttribute Relationship { get; }
203200

204-
public string Path
205-
{
206-
get
207-
{
208-
var pathBuilder = new StringBuilder();
209-
IncludeTreeNode? parent = this;
210-
211-
while (parent is { Relationship: not HiddenRootRelationshipAttribute })
212-
{
213-
pathBuilder.Insert(0, pathBuilder.Length > 0 ? $"{parent.Relationship.PublicName}." : parent.Relationship.PublicName);
214-
parent = parent._parent;
215-
}
216-
217-
return pathBuilder.ToString();
218-
}
219-
}
220-
221-
private IncludeTreeNode(RelationshipAttribute relationship, IncludeTreeNode? parent)
201+
private IncludeTreeNode(RelationshipAttribute relationship)
222202
{
223203
Relationship = relationship;
224-
_parent = parent;
225204
}
226205

227206
public static IncludeTreeNode CreateRoot(ResourceType resourceType)
228207
{
229208
var relationship = new HiddenRootRelationshipAttribute(resourceType);
230-
return new IncludeTreeNode(relationship, null);
209+
return new IncludeTreeNode(relationship);
231210
}
232211

233212
public ICollection<IncludeTreeNode> EnsureChildren(ICollection<RelationshipAttribute> relationships)
@@ -236,7 +215,7 @@ public ICollection<IncludeTreeNode> EnsureChildren(ICollection<RelationshipAttri
236215
{
237216
if (!_children.ContainsKey(relationship))
238217
{
239-
var newChild = new IncludeTreeNode(relationship, this);
218+
var newChild = new IncludeTreeNode(relationship);
240219
_children.Add(relationship, newChild);
241220
}
242221
}

src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,25 +47,27 @@ protected PaginationQueryStringValueExpression ParsePagination()
4747

4848
protected PaginationElementQueryStringValueExpression ParsePaginationElement()
4949
{
50+
int position = GetNextTokenPositionOrEnd();
5051
int? number = TryParseNumber();
5152

5253
if (number != null)
5354
{
54-
return new PaginationElementQueryStringValueExpression(null, number.Value);
55+
return new PaginationElementQueryStringValueExpression(null, number.Value, position);
5556
}
5657

5758
ResourceFieldChainExpression scope = ParseFieldChain(FieldChainRequirements.EndsInToMany, "Number or relationship name expected.");
5859

5960
EatSingleCharacterToken(TokenKind.Colon);
6061

62+
position = GetNextTokenPositionOrEnd();
6163
number = TryParseNumber();
6264

6365
if (number == null)
6466
{
65-
throw new QueryParseException("Number expected.");
67+
throw new QueryParseException("Number expected.", position);
6668
}
6769

68-
return new PaginationElementQueryStringValueExpression(scope, number.Value);
70+
return new PaginationElementQueryStringValueExpression(scope, number.Value, position);
6971
}
7072

7173
protected int? TryParseNumber()
@@ -77,13 +79,14 @@ protected PaginationElementQueryStringValueExpression ParsePaginationElement()
7779
if (nextToken.Kind == TokenKind.Minus)
7880
{
7981
TokenStack.Pop();
82+
int position = GetNextTokenPositionOrEnd();
8083

8184
if (TokenStack.TryPop(out Token? token) && token.Kind == TokenKind.Text && int.TryParse(token.Value, out number))
8285
{
8386
return -number;
8487
}
8588

86-
throw new QueryParseException("Digits expected.");
89+
throw new QueryParseException("Digits expected.", position);
8790
}
8891

8992
if (nextToken.Kind == TokenKind.Text && int.TryParse(nextToken.Value, out number))
@@ -96,8 +99,8 @@ protected PaginationElementQueryStringValueExpression ParsePaginationElement()
9699
return null;
97100
}
98101

99-
protected override IImmutableList<ResourceFieldAttribute> OnResolveFieldChain(string path, FieldChainRequirements chainRequirements)
102+
protected override IImmutableList<ResourceFieldAttribute> OnResolveFieldChain(string path, int position, FieldChainRequirements chainRequirements)
100103
{
101-
return ChainResolver.ResolveToManyChain(_resourceTypeInScope!, path);
104+
return ChainResolver.ResolveToManyChain(_resourceTypeInScope!, path, position);
102105
}
103106
}

0 commit comments

Comments
 (0)