Skip to content

Commit 5d625fa

Browse files
author
Bart Koelman
committed
Moved chain resolver logic into parsers. This is tricky to get right and now that it is integrated parsers can easily be reused from ResourceDefinitions without needing to know what type of relationships are allowed at various stages.
1 parent d5c57cb commit 5d625fa

16 files changed

+255
-173
lines changed

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

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,32 @@
33
using System.Linq;
44
using System.Reflection;
55
using Humanizer;
6+
using JsonApiDotNetCore.Internal.Contracts;
67
using JsonApiDotNetCore.Internal.Queries.Expressions;
78
using JsonApiDotNetCore.Models;
9+
using JsonApiDotNetCore.Models.Annotation;
810

911
namespace JsonApiDotNetCore.Internal.Queries.Parsing
1012
{
11-
// TODO: Combine callbacks into parsers to make them reusable from ResourceDefinitions.
1213
public class FilterParser : QueryParser
1314
{
14-
private readonly ResolveFieldChainCallback _resolveFieldChainCallback;
15-
private readonly Func<Type, string, string> _resolveStringId;
15+
private readonly IResourceFactory _resourceFactory;
16+
private readonly Action<ResourceFieldAttribute, ResourceContext, string> _validateSingleFieldCallback;
17+
private ResourceContext _resourceContextInScope;
1618

17-
public FilterParser(string source, ResolveFieldChainCallback resolveFieldChainCallback, Func<Type, string, string> resolveStringId)
18-
: base(source, resolveFieldChainCallback)
19+
public FilterParser(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory,
20+
Action<ResourceFieldAttribute, ResourceContext, string> validateSingleFieldCallback = null)
21+
: base(resourceContextProvider)
1922
{
20-
_resolveFieldChainCallback = resolveFieldChainCallback;
21-
_resolveStringId = resolveStringId ?? throw new ArgumentNullException(nameof(resolveStringId));
23+
_resourceFactory = resourceFactory ?? throw new ArgumentNullException(nameof(resourceFactory));
24+
_validateSingleFieldCallback = validateSingleFieldCallback;
2225
}
2326

24-
public FilterExpression Parse()
27+
public FilterExpression Parse(string source, ResourceContext resourceContextInScope)
2528
{
29+
Tokenize(source);
30+
_resourceContextInScope = resourceContextInScope ?? throw new ArgumentNullException(nameof(resourceContextInScope));
31+
2632
var expression = ParseFilter();
2733

2834
AssertTokenStackIsEmpty();
@@ -139,13 +145,13 @@ protected ComparisonExpression ParseComparison(string operatorName)
139145
!(rightTerm is NullConstantExpression))
140146
{
141147
// Run another pass over left chain to have it fail when chain ends in relationship.
142-
_resolveFieldChainCallback(leftChain.ToString(), FieldChainRequirements.EndsInAttribute);
148+
OnResolveFieldChain(leftChain.ToString(), FieldChainRequirements.EndsInAttribute);
143149
}
144150

145151
PropertyInfo leftProperty = leftChain.Fields.Last().Property;
146152
if (leftProperty.Name == nameof(Identifiable.Id) && rightTerm is LiteralConstantExpression rightConstant)
147153
{
148-
string id = _resolveStringId(leftProperty.ReflectedType, rightConstant.Value);
154+
string id = DeObfuscateStringId(leftProperty.ReflectedType, rightConstant.Value);
149155
rightTerm = new LiteralConstantExpression(id);
150156
}
151157
}
@@ -205,7 +211,7 @@ protected EqualsAnyOfExpression ParseAny()
205211
for (int index = 0; index < constants.Count; index++)
206212
{
207213
string stringId = constants[index].Value;
208-
string id = _resolveStringId(targetAttributeProperty.ReflectedType, stringId);
214+
string id = DeObfuscateStringId(targetAttributeProperty.ReflectedType, stringId);
209215
constants[index] = new LiteralConstantExpression(id);
210216
}
211217
}
@@ -285,5 +291,31 @@ protected LiteralConstantExpression ParseConstant()
285291

286292
throw new QueryParseException("Value between quotes expected.");
287293
}
294+
295+
private string DeObfuscateStringId(Type resourceType, string stringId)
296+
{
297+
return TypeHelper.ConvertStringIdToTypedId(resourceType, stringId, _resourceFactory).ToString();
298+
}
299+
300+
protected override IReadOnlyCollection<ResourceFieldAttribute> OnResolveFieldChain(string path, FieldChainRequirements chainRequirements)
301+
{
302+
if (chainRequirements == FieldChainRequirements.EndsInToMany)
303+
{
304+
return ChainResolver.ResolveToOneChainEndingInToMany(_resourceContextInScope, path, _validateSingleFieldCallback);
305+
}
306+
307+
if (chainRequirements == FieldChainRequirements.EndsInAttribute)
308+
{
309+
return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceContextInScope, path, _validateSingleFieldCallback);
310+
}
311+
312+
if (chainRequirements.HasFlag(FieldChainRequirements.EndsInAttribute) &&
313+
chainRequirements.HasFlag(FieldChainRequirements.EndsInToOne))
314+
{
315+
return ChainResolver.ResolveToOneChainEndingInAttributeOrToOne(_resourceContextInScope, path, _validateSingleFieldCallback);
316+
}
317+
318+
throw new InvalidOperationException($"Unexpected combination of chain requirement flags '{chainRequirements}'.");
319+
}
288320
}
289321
}

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Linq;
4+
using JsonApiDotNetCore.Internal.Contracts;
35
using JsonApiDotNetCore.Internal.Queries.Expressions;
6+
using JsonApiDotNetCore.Models.Annotation;
47

58
namespace JsonApiDotNetCore.Internal.Queries.Parsing
69
{
710
public class IncludeParser : QueryParser
811
{
9-
public IncludeParser(string source, ResolveFieldChainCallback resolveFieldChainCallback)
10-
: base(source, resolveFieldChainCallback)
12+
private readonly Action<RelationshipAttribute, ResourceContext, string> _validateSingleRelationshipCallback;
13+
private ResourceContext _resourceContextInScope;
14+
15+
public IncludeParser(IResourceContextProvider resourceContextProvider,
16+
Action<RelationshipAttribute, ResourceContext, string> validateSingleRelationshipCallback = null)
17+
: base(resourceContextProvider)
1118
{
19+
_validateSingleRelationshipCallback = validateSingleRelationshipCallback;
1220
}
1321

14-
public IncludeExpression Parse()
22+
public IncludeExpression Parse(string source, ResourceContext resourceContextInScope)
1523
{
24+
_resourceContextInScope = resourceContextInScope ?? throw new ArgumentNullException(nameof(resourceContextInScope));
25+
Tokenize(source);
26+
1627
var expression = ParseInclude();
1728

1829
AssertTokenStackIsEmpty();
@@ -40,5 +51,10 @@ protected IncludeExpression ParseInclude()
4051

4152
return IncludeChainConverter.FromRelationshipChains(chains);
4253
}
54+
55+
protected override IReadOnlyCollection<ResourceFieldAttribute> OnResolveFieldChain(string path, FieldChainRequirements chainRequirements)
56+
{
57+
return ChainResolver.ResolveRelationshipChain(_resourceContextInScope, path, _validateSingleRelationshipCallback);
58+
}
4359
}
4460
}

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Linq;
4+
using JsonApiDotNetCore.Internal.Contracts;
35
using JsonApiDotNetCore.Internal.Queries.Expressions;
6+
using JsonApiDotNetCore.Models.Annotation;
47

58
namespace JsonApiDotNetCore.Internal.Queries.Parsing
69
{
710
public class PaginationParser : QueryParser
811
{
9-
public PaginationParser(string source, ResolveFieldChainCallback resolveFieldChainCallback)
10-
: base(source, resolveFieldChainCallback)
12+
private readonly Action<ResourceFieldAttribute, ResourceContext, string> _validateSingleFieldCallback;
13+
private ResourceContext _resourceContextInScope;
14+
15+
public PaginationParser(IResourceContextProvider resourceContextProvider,
16+
Action<ResourceFieldAttribute, ResourceContext, string> validateSingleFieldCallback = null)
17+
: base(resourceContextProvider)
1118
{
19+
_validateSingleFieldCallback = validateSingleFieldCallback;
1220
}
1321

14-
public PaginationQueryStringValueExpression Parse()
22+
public PaginationQueryStringValueExpression Parse(string source, ResourceContext resourceContextInScope)
1523
{
24+
_resourceContextInScope = resourceContextInScope ?? throw new ArgumentNullException(nameof(resourceContextInScope));
25+
Tokenize(source);
26+
1627
var expression = ParsePagination();
1728

1829
AssertTokenStackIsEmpty();
@@ -87,5 +98,10 @@ protected PaginationElementQueryStringValueExpression ParsePaginationElement()
8798

8899
return null;
89100
}
101+
102+
protected override IReadOnlyCollection<ResourceFieldAttribute> OnResolveFieldChain(string path, FieldChainRequirements chainRequirements)
103+
{
104+
return ChainResolver.ResolveToManyChain(_resourceContextInScope, path, _validateSingleFieldCallback);
105+
}
90106
}
91107
}

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
1-
using System;
21
using System.Collections.Generic;
32
using System.Linq;
3+
using JsonApiDotNetCore.Internal.Contracts;
44
using JsonApiDotNetCore.Internal.Queries.Expressions;
5+
using JsonApiDotNetCore.Internal.QueryStrings;
6+
using JsonApiDotNetCore.Models.Annotation;
57

68
namespace JsonApiDotNetCore.Internal.Queries.Parsing
79
{
810
public abstract class QueryParser
911
{
10-
private readonly ResolveFieldChainCallback _resolveFieldChainCallback;
12+
private protected ResourceFieldChainResolver ChainResolver { get; }
1113

12-
protected Stack<Token> TokenStack { get; }
14+
protected Stack<Token> TokenStack { get; private set; }
1315

14-
protected QueryParser(string source, ResolveFieldChainCallback resolveFieldChainCallback)
16+
protected QueryParser(IResourceContextProvider resourceContextProvider)
1517
{
16-
_resolveFieldChainCallback = resolveFieldChainCallback ?? throw new ArgumentNullException(nameof(resolveFieldChainCallback));
18+
ChainResolver = new ResourceFieldChainResolver(resourceContextProvider);
19+
}
20+
21+
protected abstract IReadOnlyCollection<ResourceFieldAttribute> OnResolveFieldChain(string path, FieldChainRequirements chainRequirements);
1722

23+
protected virtual void Tokenize(string source)
24+
{
1825
var tokenizer = new QueryTokenizer(source);
1926
TokenStack = new Stack<Token>(tokenizer.EnumerateTokens().Reverse());
2027
}
@@ -23,7 +30,7 @@ protected ResourceFieldChainExpression ParseFieldChain(FieldChainRequirements ch
2330
{
2431
if (TokenStack.TryPop(out Token token) && token.Kind == TokenKind.Text)
2532
{
26-
var chain = _resolveFieldChainCallback(token.Value, chainRequirements);
33+
var chain = OnResolveFieldChain(token.Value, chainRequirements);
2734
if (chain.Any())
2835
{
2936
return new ResourceFieldChainExpression(chain);

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

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,38 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using JsonApiDotNetCore.Internal.Contracts;
14
using JsonApiDotNetCore.Internal.Queries.Expressions;
5+
using JsonApiDotNetCore.Models.Annotation;
26

37
namespace JsonApiDotNetCore.Internal.Queries.Parsing
48
{
59
public class QueryStringParameterScopeParser : QueryParser
610
{
7-
public QueryStringParameterScopeParser(string source, ResolveFieldChainCallback resolveFieldChainCallback)
8-
: base(source, resolveFieldChainCallback)
11+
private readonly FieldChainRequirements _chainRequirements;
12+
private readonly Action<ResourceFieldAttribute, ResourceContext, string> _validateSingleFieldCallback;
13+
private ResourceContext _resourceContextInScope;
14+
15+
public QueryStringParameterScopeParser(IResourceContextProvider resourceContextProvider, FieldChainRequirements chainRequirements,
16+
Action<ResourceFieldAttribute, ResourceContext, string> validateSingleFieldCallback = null)
17+
: base(resourceContextProvider)
918
{
19+
_chainRequirements = chainRequirements;
20+
_validateSingleFieldCallback = validateSingleFieldCallback;
1021
}
1122

12-
public QueryStringParameterScopeExpression Parse(FieldChainRequirements chainRequirements)
23+
public QueryStringParameterScopeExpression Parse(string source, ResourceContext resourceContextInScope)
1324
{
14-
var expression = ParseQueryStringParameterScope(chainRequirements);
25+
_resourceContextInScope = resourceContextInScope ?? throw new ArgumentNullException(nameof(resourceContextInScope));
26+
Tokenize(source);
27+
28+
var expression = ParseQueryStringParameterScope();
1529

1630
AssertTokenStackIsEmpty();
1731

1832
return expression;
1933
}
2034

21-
protected QueryStringParameterScopeExpression ParseQueryStringParameterScope(FieldChainRequirements chainRequirements)
35+
protected QueryStringParameterScopeExpression ParseQueryStringParameterScope()
2236
{
2337
if (!TokenStack.TryPop(out Token token) || token.Kind != TokenKind.Text)
2438
{
@@ -33,12 +47,27 @@ protected QueryStringParameterScopeExpression ParseQueryStringParameterScope(Fie
3347
{
3448
TokenStack.Pop();
3549

36-
scope = ParseFieldChain(chainRequirements, null);
50+
scope = ParseFieldChain(_chainRequirements, null);
3751

3852
EatSingleCharacterToken(TokenKind.CloseBracket);
3953
}
4054

4155
return new QueryStringParameterScopeExpression(name, scope);
4256
}
57+
58+
protected override IReadOnlyCollection<ResourceFieldAttribute> OnResolveFieldChain(string path, FieldChainRequirements chainRequirements)
59+
{
60+
if (chainRequirements == FieldChainRequirements.EndsInToMany)
61+
{
62+
return ChainResolver.ResolveToManyChain(_resourceContextInScope, path, _validateSingleFieldCallback);
63+
}
64+
65+
if (chainRequirements == FieldChainRequirements.IsRelationship)
66+
{
67+
return ChainResolver.ResolveRelationshipChain(_resourceContextInScope, path, _validateSingleFieldCallback);
68+
}
69+
70+
throw new InvalidOperationException($"Unexpected combination of chain requirement flags '{chainRequirements}'.");
71+
}
4372
}
4473
}

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

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Linq;
4+
using JsonApiDotNetCore.Internal.Contracts;
35
using JsonApiDotNetCore.Internal.Queries.Expressions;
6+
using JsonApiDotNetCore.Models.Annotation;
47

58
namespace JsonApiDotNetCore.Internal.Queries.Parsing
69
{
710
public class SortParser : QueryParser
811
{
9-
public SortParser(string source, ResolveFieldChainCallback resolveFieldChainCallback)
10-
: base(source, resolveFieldChainCallback)
12+
private readonly Action<ResourceFieldAttribute, ResourceContext, string> _validateSingleFieldCallback;
13+
private ResourceContext _resourceContextInScope;
14+
15+
public SortParser(IResourceContextProvider resourceContextProvider,
16+
Action<ResourceFieldAttribute, ResourceContext, string> validateSingleFieldCallback = null)
17+
: base(resourceContextProvider)
1118
{
19+
_validateSingleFieldCallback = validateSingleFieldCallback;
1220
}
1321

14-
public SortExpression Parse()
22+
public SortExpression Parse(string source, ResourceContext resourceContextInScope)
1523
{
24+
_resourceContextInScope = resourceContextInScope ?? throw new ArgumentNullException(nameof(resourceContextInScope));
25+
Tokenize(source);
26+
1627
SortExpression expression = ParseSort();
1728

1829
AssertTokenStackIsEmpty();
@@ -60,5 +71,20 @@ protected SortElementExpression ParseSortElement()
6071
ResourceFieldChainExpression targetAttribute = ParseFieldChain(FieldChainRequirements.EndsInAttribute, errorMessage);
6172
return new SortElementExpression(targetAttribute, isAscending);
6273
}
74+
75+
protected override IReadOnlyCollection<ResourceFieldAttribute> OnResolveFieldChain(string path, FieldChainRequirements chainRequirements)
76+
{
77+
if (chainRequirements == FieldChainRequirements.EndsInToMany)
78+
{
79+
return ChainResolver.ResolveToOneChainEndingInToMany(_resourceContextInScope, path);
80+
}
81+
82+
if (chainRequirements == FieldChainRequirements.EndsInAttribute)
83+
{
84+
return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceContextInScope, path, _validateSingleFieldCallback);
85+
}
86+
87+
throw new InvalidOperationException($"Unexpected combination of chain requirement flags '{chainRequirements}'.");
88+
}
6389
}
6490
}

0 commit comments

Comments
 (0)