Skip to content

Commit 6ebcaa6

Browse files
author
Bart Koelman
committed
Resource inheritance: derived filters
1 parent 2948f6c commit 6ebcaa6

File tree

12 files changed

+502
-16
lines changed

12 files changed

+502
-16
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using System.Text;
2+
using JetBrains.Annotations;
3+
using JsonApiDotNetCore.Configuration;
4+
using JsonApiDotNetCore.Queries.Internal.Parsing;
5+
6+
namespace JsonApiDotNetCore.Queries.Expressions;
7+
8+
/// <summary>
9+
/// Represents the "isType" filter function, resulting from text such as: isType(men), isType(men:creator) or isType(men:creator,equals(hasBeard,'true'))
10+
/// </summary>
11+
[PublicAPI]
12+
public class IsTypeExpression : FilterExpression
13+
{
14+
public ResourceType DerivedType { get; }
15+
public ResourceFieldChainExpression? TargetToOneRelationship { get; }
16+
public FilterExpression? Child { get; }
17+
18+
public IsTypeExpression(ResourceType derivedType, ResourceFieldChainExpression? targetToOneRelationship, FilterExpression? child)
19+
{
20+
ArgumentGuard.NotNull(derivedType, nameof(derivedType));
21+
22+
DerivedType = derivedType;
23+
TargetToOneRelationship = targetToOneRelationship;
24+
Child = child;
25+
}
26+
27+
public override TResult Accept<TArgument, TResult>(QueryExpressionVisitor<TArgument, TResult> visitor, TArgument argument)
28+
{
29+
return visitor.VisitIsType(this, argument);
30+
}
31+
32+
public override string ToString()
33+
{
34+
var builder = new StringBuilder();
35+
builder.Append(Keywords.IsType);
36+
builder.Append('(');
37+
builder.Append(DerivedType);
38+
39+
if (TargetToOneRelationship != null)
40+
{
41+
builder.Append(':');
42+
builder.Append(TargetToOneRelationship);
43+
}
44+
45+
if (Child != null)
46+
{
47+
builder.Append(',');
48+
builder.Append(Child);
49+
}
50+
51+
builder.Append(')');
52+
53+
return builder.ToString();
54+
}
55+
56+
public override bool Equals(object? obj)
57+
{
58+
if (ReferenceEquals(this, obj))
59+
{
60+
return true;
61+
}
62+
63+
if (obj is null || GetType() != obj.GetType())
64+
{
65+
return false;
66+
}
67+
68+
var other = (IsTypeExpression)obj;
69+
70+
return DerivedType.Equals(other.DerivedType) && Equals(TargetToOneRelationship, other.TargetToOneRelationship) && Equals(Child, other.Child);
71+
}
72+
73+
public override int GetHashCode()
74+
{
75+
return HashCode.Combine(DerivedType, TargetToOneRelationship, Child);
76+
}
77+
}

src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs

+12
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,18 @@ public override QueryExpression VisitNullConstant(NullConstantExpression express
9191
return null;
9292
}
9393

94+
public override QueryExpression VisitIsType(IsTypeExpression expression, TArgument argument)
95+
{
96+
ResourceFieldChainExpression? newTargetToOneRelationship = expression.TargetToOneRelationship != null
97+
? Visit(expression.TargetToOneRelationship, argument) as ResourceFieldChainExpression
98+
: null;
99+
100+
FilterExpression? newChild = expression.Child != null ? Visit(expression.Child, argument) as FilterExpression : null;
101+
102+
var newExpression = new IsTypeExpression(expression.DerivedType, newTargetToOneRelationship, newChild);
103+
return newExpression.Equals(expression) ? expression : newExpression;
104+
}
105+
94106
public override QueryExpression? VisitSortElement(SortElementExpression expression, TArgument argument)
95107
{
96108
SortElementExpression? newExpression = null;

src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs

+5
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ public virtual TResult VisitHas(HasExpression expression, TArgument argument)
5353
return DefaultVisit(expression, argument);
5454
}
5555

56+
public virtual TResult VisitIsType(IsTypeExpression expression, TArgument argument)
57+
{
58+
return DefaultVisit(expression, argument);
59+
}
60+
5661
public virtual TResult VisitSortElement(SortElementExpression expression, TArgument argument)
5762
{
5863
return DefaultVisit(expression, argument);

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

+110-10
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,16 @@ public FilterExpression Parse(string source, ResourceType resourceTypeInScope)
2828
{
2929
ArgumentGuard.NotNull(resourceTypeInScope, nameof(resourceTypeInScope));
3030

31-
_resourceTypeInScope = resourceTypeInScope;
32-
33-
Tokenize(source);
31+
return InScopeOfResourceType(resourceTypeInScope, () =>
32+
{
33+
Tokenize(source);
3434

35-
FilterExpression expression = ParseFilter();
35+
FilterExpression expression = ParseFilter();
3636

37-
AssertTokenStackIsEmpty();
37+
AssertTokenStackIsEmpty();
3838

39-
return expression;
39+
return expression;
40+
});
4041
}
4142

4243
protected FilterExpression ParseFilter()
@@ -76,6 +77,10 @@ protected FilterExpression ParseFilter()
7677
{
7778
return ParseHas();
7879
}
80+
case Keywords.IsType:
81+
{
82+
return ParseIsType();
83+
}
7984
}
8085
}
8186

@@ -259,13 +264,88 @@ protected HasExpression ParseHas()
259264

260265
private FilterExpression ParseFilterInHas(HasManyAttribute hasManyRelationship)
261266
{
262-
ResourceType outerScopeBackup = _resourceTypeInScope!;
267+
return InScopeOfResourceType(hasManyRelationship.RightType, ParseFilter);
268+
}
269+
270+
private IsTypeExpression ParseIsType()
271+
{
272+
EatText(Keywords.IsType);
273+
EatSingleCharacterToken(TokenKind.OpenParen);
274+
275+
string derivedTypeName = EatResourceTypeName();
276+
277+
ResourceType baseType = _resourceTypeInScope!;
278+
ResourceFieldChainExpression? targetToOneRelationship = null;
279+
280+
if (TokenStack.TryPeek(out Token? nextToken) && nextToken.Kind == TokenKind.Colon)
281+
{
282+
EatSingleCharacterToken(TokenKind.Colon);
283+
284+
targetToOneRelationship = ParseFieldChain(FieldChainRequirements.EndsInToOne, "Relationship name expected.");
285+
baseType = ((RelationshipAttribute)targetToOneRelationship.Fields[^1]).RightType;
286+
}
287+
288+
ResourceType derivedType = ResolveDerivedType(baseType, derivedTypeName);
289+
FilterExpression? child = TryParseFilterInIsType(derivedType);
290+
291+
EatSingleCharacterToken(TokenKind.CloseParen);
292+
293+
return new IsTypeExpression(derivedType, targetToOneRelationship, child);
294+
}
295+
296+
private string EatResourceTypeName()
297+
{
298+
if (TokenStack.TryPop(out Token? token) && token.Kind == TokenKind.Text)
299+
{
300+
return token.Value!;
301+
}
302+
303+
throw new QueryParseException("Resource type expected.");
304+
}
305+
306+
private ResourceType ResolveDerivedType(ResourceType baseType, string derivedTypeName)
307+
{
308+
ResourceType? derivedType = GetDerivedType(baseType, derivedTypeName);
263309

264-
_resourceTypeInScope = hasManyRelationship.RightType;
310+
if (derivedType == null)
311+
{
312+
throw new QueryParseException($"Resource type '{derivedTypeName}' does not exist or does not derive from '{baseType.PublicName}'.");
313+
}
265314

266-
FilterExpression filter = ParseFilter();
315+
return derivedType;
316+
}
317+
318+
private ResourceType? GetDerivedType(ResourceType baseType, string publicName)
319+
{
320+
foreach (ResourceType derivedType in baseType.DirectlyDerivedTypes)
321+
{
322+
if (derivedType.PublicName == publicName)
323+
{
324+
return derivedType;
325+
}
326+
327+
ResourceType? nextType = GetDerivedType(derivedType, publicName);
328+
329+
if (nextType != null)
330+
{
331+
return nextType;
332+
}
333+
}
334+
335+
return null;
336+
}
337+
338+
private FilterExpression? TryParseFilterInIsType(ResourceType derivedType)
339+
{
340+
FilterExpression? filter = null;
341+
342+
if (TokenStack.TryPeek(out Token? nextToken) && nextToken.Kind == TokenKind.Comma)
343+
{
344+
EatSingleCharacterToken(TokenKind.Comma);
345+
346+
filter = InScopeOfResourceType(derivedType, ParseFilter);
347+
}
267348

268-
_resourceTypeInScope = outerScopeBackup;
269349
return filter;
270350
}
271351

@@ -349,11 +429,31 @@ protected override IImmutableList<ResourceFieldAttribute> OnResolveFieldChain(st
349429
return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceTypeInScope!, path, _validateSingleFieldCallback);
350430
}
351431

432+
if (chainRequirements == FieldChainRequirements.EndsInToOne)
433+
{
434+
return ChainResolver.ResolveToOneChain(_resourceTypeInScope!, path, _validateSingleFieldCallback);
435+
}
436+
352437
if (chainRequirements.HasFlag(FieldChainRequirements.EndsInAttribute) && chainRequirements.HasFlag(FieldChainRequirements.EndsInToOne))
353438
{
354439
return ChainResolver.ResolveToOneChainEndingInAttributeOrToOne(_resourceTypeInScope!, path, _validateSingleFieldCallback);
355440
}
356441

357442
throw new InvalidOperationException($"Unexpected combination of chain requirement flags '{chainRequirements}'.");
358443
}
444+
445+
private TResult InScopeOfResourceType<TResult>(ResourceType resourceType, Func<TResult> action)
446+
{
447+
ResourceType? backupType = _resourceTypeInScope;
448+
449+
try
450+
{
451+
_resourceTypeInScope = resourceType;
452+
return action();
453+
}
454+
finally
455+
{
456+
_resourceTypeInScope = backupType;
457+
}
458+
}
359459
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ public static class Keywords
2323
public const string Any = "any";
2424
public const string Count = "count";
2525
public const string Has = "has";
26+
public const string IsType = "isType";
2627
}

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

+26
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,32 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing;
99
/// </summary>
1010
internal sealed class ResourceFieldChainResolver
1111
{
12+
/// <summary>
13+
/// Resolves a chain of to-one relationships.
14+
/// <example>author</example>
15+
/// <example>
16+
/// author.address.country
17+
/// </example>
18+
/// </summary>
19+
public IImmutableList<ResourceFieldAttribute> ResolveToOneChain(ResourceType resourceType, string path,
20+
Action<ResourceFieldAttribute, ResourceType, string>? validateCallback = null)
21+
{
22+
ImmutableArray<ResourceFieldAttribute>.Builder chainBuilder = ImmutableArray.CreateBuilder<ResourceFieldAttribute>();
23+
ResourceType nextResourceType = resourceType;
24+
25+
foreach (string publicName in path.Split("."))
26+
{
27+
RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceType, path);
28+
29+
validateCallback?.Invoke(toOneRelationship, nextResourceType, path);
30+
31+
chainBuilder.Add(toOneRelationship);
32+
nextResourceType = toOneRelationship.RightType;
33+
}
34+
35+
return chainBuilder.ToImmutable();
36+
}
37+
1238
/// <summary>
1339
/// Resolves a chain of relationships that ends in a to-many relationship, for example: blogs.owner.articles.comments
1440
/// </summary>

src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScope.cs

+19-4
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,30 @@ public sealed class LambdaScope : IDisposable
1414
public ParameterExpression Parameter { get; }
1515
public Expression Accessor { get; }
1616

17-
public LambdaScope(LambdaParameterNameFactory nameFactory, Type elementType, Expression? accessorExpression)
17+
private LambdaScope(LambdaParameterNameScope parameterNameScope, ParameterExpression parameter, Expression accessor)
18+
{
19+
_parameterNameScope = parameterNameScope;
20+
Parameter = parameter;
21+
Accessor = accessor;
22+
}
23+
24+
public static LambdaScope Create(LambdaParameterNameFactory nameFactory, Type elementType, Expression? accessorExpression)
1825
{
1926
ArgumentGuard.NotNull(nameFactory, nameof(nameFactory));
2027
ArgumentGuard.NotNull(elementType, nameof(elementType));
2128

22-
_parameterNameScope = nameFactory.Create(elementType.Name);
23-
Parameter = Expression.Parameter(elementType, _parameterNameScope.Name);
29+
LambdaParameterNameScope parameterNameScope = nameFactory.Create(elementType.Name);
30+
ParameterExpression parameter = Expression.Parameter(elementType, parameterNameScope.Name);
31+
Expression accessor = accessorExpression ?? parameter;
32+
33+
return new LambdaScope(parameterNameScope, parameter, accessor);
34+
}
35+
36+
public LambdaScope WithAccessor(Expression accessorExpression)
37+
{
38+
ArgumentGuard.NotNull(accessorExpression, nameof(accessorExpression));
2439

25-
Accessor = accessorExpression ?? Parameter;
40+
return new LambdaScope(_parameterNameScope, Parameter, accessorExpression);
2641
}
2742

2843
public void Dispose()

src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScopeFactory.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ public LambdaScope CreateScope(Type elementType, Expression? accessorExpression
1919
{
2020
ArgumentGuard.NotNull(elementType, nameof(elementType));
2121

22-
return new LambdaScope(_nameFactory, elementType, accessorExpression);
22+
return LambdaScope.Create(_nameFactory, elementType, accessorExpression);
2323
}
2424
}

src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs

+21-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding;
99
/// </summary>
1010
public abstract class QueryClauseBuilder<TArgument> : QueryExpressionVisitor<TArgument, Expression>
1111
{
12-
protected LambdaScope LambdaScope { get; }
12+
protected LambdaScope LambdaScope { get; private set; }
1313

1414
protected QueryClauseBuilder(LambdaScope lambdaScope)
1515
{
@@ -83,4 +83,24 @@ private static MemberExpression CreatePropertyExpressionFromComponents(Expressio
8383

8484
return property!;
8585
}
86+
87+
protected TResult WithLambdaScopeAccessor<TResult>(Expression accessorExpression, Func<TResult> action)
88+
{
89+
ArgumentGuard.NotNull(accessorExpression, nameof(accessorExpression));
90+
ArgumentGuard.NotNull(action, nameof(action));
91+
92+
LambdaScope backupScope = LambdaScope;
93+
94+
try
95+
{
96+
using (LambdaScope = LambdaScope.WithAccessor(accessorExpression))
97+
{
98+
return action();
99+
}
100+
}
101+
finally
102+
{
103+
LambdaScope = backupScope;
104+
}
105+
}
86106
}

0 commit comments

Comments
 (0)