Skip to content

Commit ae645c9

Browse files
author
Bart Koelman
committed
Resource inheritance: derived filters
1 parent d95d164 commit ae645c9

File tree

13 files changed

+509
-17
lines changed

13 files changed

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

src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs

Lines changed: 12 additions & 0 deletions
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(newTargetToOneRelationship, expression.DerivedType, 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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 114 additions & 10 deletions
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,92 @@ 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+
ResourceFieldChainExpression? targetToOneRelationship = TryParseToOneRelationshipChain();
276+
277+
EatSingleCharacterToken(TokenKind.Comma);
278+
279+
ResourceType baseType = targetToOneRelationship != null ? ((RelationshipAttribute)targetToOneRelationship.Fields[^1]).RightType : _resourceTypeInScope!;
280+
ResourceType derivedType = ParseDerivedType(baseType);
281+
282+
FilterExpression? child = TryParseFilterInIsType(derivedType);
283+
284+
EatSingleCharacterToken(TokenKind.CloseParen);
285+
286+
return new IsTypeExpression(targetToOneRelationship, derivedType, child);
287+
}
288+
289+
private ResourceFieldChainExpression? TryParseToOneRelationshipChain()
290+
{
291+
if (TokenStack.TryPeek(out Token? nextToken) && nextToken.Kind == TokenKind.Comma)
292+
{
293+
return null;
294+
}
295+
296+
return ParseFieldChain(FieldChainRequirements.EndsInToOne, "Relationship name or , expected.");
297+
}
298+
299+
private ResourceType ParseDerivedType(ResourceType baseType)
300+
{
301+
if (TokenStack.TryPop(out Token? token) && token.Kind == TokenKind.Text)
302+
{
303+
string derivedTypeName = token.Value!;
304+
return ResolveDerivedType(baseType, derivedTypeName);
305+
}
306+
307+
throw new QueryParseException("Resource type expected.");
308+
}
309+
310+
private ResourceType ResolveDerivedType(ResourceType baseType, string derivedTypeName)
311+
{
312+
ResourceType? derivedType = GetDerivedType(baseType, derivedTypeName);
313+
314+
if (derivedType == null)
315+
{
316+
throw new QueryParseException($"Resource type '{derivedTypeName}' does not exist or does not derive from '{baseType.PublicName}'.");
317+
}
318+
319+
return derivedType;
320+
}
321+
322+
private ResourceType? GetDerivedType(ResourceType baseType, string publicName)
323+
{
324+
foreach (ResourceType derivedType in baseType.DirectlyDerivedTypes)
325+
{
326+
if (derivedType.PublicName == publicName)
327+
{
328+
return derivedType;
329+
}
330+
331+
ResourceType? nextType = GetDerivedType(derivedType, publicName);
332+
333+
if (nextType != null)
334+
{
335+
return nextType;
336+
}
337+
}
338+
339+
return null;
340+
}
341+
342+
private FilterExpression? TryParseFilterInIsType(ResourceType derivedType)
343+
{
344+
FilterExpression? filter = null;
263345

264-
_resourceTypeInScope = hasManyRelationship.RightType;
346+
if (TokenStack.TryPeek(out Token? nextToken) && nextToken.Kind == TokenKind.Comma)
347+
{
348+
EatSingleCharacterToken(TokenKind.Comma);
265349

266-
FilterExpression filter = ParseFilter();
350+
filter = InScopeOfResourceType(derivedType, ParseFilter);
351+
}
267352

268-
_resourceTypeInScope = outerScopeBackup;
269353
return filter;
270354
}
271355

@@ -349,11 +433,31 @@ protected override IImmutableList<ResourceFieldAttribute> OnResolveFieldChain(st
349433
return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceTypeInScope!, path, _validateSingleFieldCallback);
350434
}
351435

436+
if (chainRequirements == FieldChainRequirements.EndsInToOne)
437+
{
438+
return ChainResolver.ResolveToOneChain(_resourceTypeInScope!, path, _validateSingleFieldCallback);
439+
}
440+
352441
if (chainRequirements.HasFlag(FieldChainRequirements.EndsInAttribute) && chainRequirements.HasFlag(FieldChainRequirements.EndsInToOne))
353442
{
354443
return ChainResolver.ResolveToOneChainEndingInAttributeOrToOne(_resourceTypeInScope!, path, _validateSingleFieldCallback);
355444
}
356445

357446
throw new InvalidOperationException($"Unexpected combination of chain requirement flags '{chainRequirements}'.");
358447
}
448+
449+
private TResult InScopeOfResourceType<TResult>(ResourceType resourceType, Func<TResult> action)
450+
{
451+
ResourceType? backupType = _resourceTypeInScope;
452+
453+
try
454+
{
455+
_resourceTypeInScope = resourceType;
456+
return action();
457+
}
458+
finally
459+
{
460+
_resourceTypeInScope = backupType;
461+
}
462+
}
359463
}

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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 26 additions & 0 deletions
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

Lines changed: 19 additions & 4 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 21 additions & 1 deletion
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
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,8 @@ private static void IncludeEagerLoads(ResourceType resourceType, Dictionary<Prop
194194

195195
private MemberAssignment CreatePropertyAssignment(PropertySelector propertySelector, LambdaScope lambdaScope)
196196
{
197-
bool requiresUpCast = !propertySelector.Property.DeclaringType!.IsAssignableFrom(lambdaScope.Accessor.Type);
197+
bool requiresUpCast = lambdaScope.Accessor.Type != propertySelector.Property.DeclaringType &&
198+
lambdaScope.Accessor.Type.IsAssignableFrom(propertySelector.Property.DeclaringType);
198199

199200
MemberExpression propertyAccess = requiresUpCast
200201
? Expression.MakeMemberAccess(Expression.Convert(lambdaScope.Accessor, propertySelector.Property.DeclaringType!), propertySelector.Property)

0 commit comments

Comments
 (0)