Skip to content

Commit 5551bed

Browse files
committed
Move parsing of include chains into base class
1 parent 77b1524 commit 5551bed

File tree

2 files changed

+223
-225
lines changed

2 files changed

+223
-225
lines changed
Lines changed: 0 additions & 223 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
using System.Collections.Immutable;
2-
using System.Collections.ObjectModel;
3-
using System.Text;
41
using JetBrains.Annotations;
52
using JsonApiDotNetCore.Configuration;
6-
using JsonApiDotNetCore.Errors;
73
using JsonApiDotNetCore.Queries.Expressions;
84
using JsonApiDotNetCore.Resources.Annotations;
95

@@ -61,158 +57,6 @@ protected virtual IncludeExpression ParseInclude(ResourceType resourceType)
6157
return treeRoot.ToExpression();
6258
}
6359

64-
private void ParseRelationshipChain(IncludeTreeNode treeRoot)
65-
{
66-
// A relationship name usually matches a single relationship, even when overridden in derived types.
67-
// But in the following case, two relationships are matched on GET /shoppingBaskets?include=items:
68-
//
69-
// public abstract class ShoppingBasket : Identifiable<long>
70-
// {
71-
// }
72-
//
73-
// public sealed class SilverShoppingBasket : ShoppingBasket
74-
// {
75-
// [HasMany]
76-
// public ISet<Article> Items { get; get; }
77-
// }
78-
//
79-
// public sealed class PlatinumShoppingBasket : ShoppingBasket
80-
// {
81-
// [HasMany]
82-
// public ISet<Product> Items { get; get; }
83-
// }
84-
//
85-
// Now if the include chain has subsequent relationships, we need to scan both Items relationships for matches,
86-
// which is why ParseRelationshipName returns a collection.
87-
//
88-
// The advantage of this unfolding is we don't require callers to upcast in relationship chains. The downside is
89-
// that there's currently no way to include Products without Articles. We could add such optional upcast syntax
90-
// in the future, if desired.
91-
92-
ReadOnlyCollection<IncludeTreeNode> children = ParseRelationshipName([treeRoot]);
93-
94-
while (TokenStack.TryPeek(out Token? nextToken) && nextToken.Kind == TokenKind.Period)
95-
{
96-
EatSingleCharacterToken(TokenKind.Period);
97-
98-
children = ParseRelationshipName(children);
99-
}
100-
}
101-
102-
private ReadOnlyCollection<IncludeTreeNode> ParseRelationshipName(IReadOnlyCollection<IncludeTreeNode> parents)
103-
{
104-
int position = GetNextTokenPositionOrEnd();
105-
106-
if (TokenStack.TryPop(out Token? token) && token.Kind == TokenKind.Text)
107-
{
108-
return LookupRelationshipName(token.Value!, parents, position);
109-
}
110-
111-
throw new QueryParseException("Relationship name expected.", position);
112-
}
113-
114-
private ReadOnlyCollection<IncludeTreeNode> LookupRelationshipName(string relationshipName, IReadOnlyCollection<IncludeTreeNode> parents, int position)
115-
{
116-
List<IncludeTreeNode> children = [];
117-
HashSet<RelationshipAttribute> relationshipsFound = [];
118-
119-
foreach (IncludeTreeNode parent in parents)
120-
{
121-
// Depending on the left side of the include chain, we may match relationships anywhere in the resource type hierarchy.
122-
// This is compensated for when rendering the response, which substitutes relationships on base types with the derived ones.
123-
HashSet<RelationshipAttribute> relationships = GetRelationshipsInConcreteTypes(parent.Relationship.RightType, relationshipName);
124-
125-
if (relationships.Count > 0)
126-
{
127-
relationshipsFound.UnionWith(relationships);
128-
129-
RelationshipAttribute[] relationshipsToInclude = relationships.Where(relationship => !relationship.IsIncludeBlocked()).ToArray();
130-
ReadOnlyCollection<IncludeTreeNode> affectedChildren = parent.EnsureChildren(relationshipsToInclude);
131-
children.AddRange(affectedChildren);
132-
}
133-
}
134-
135-
AssertRelationshipsFound(relationshipsFound, relationshipName, parents, position);
136-
AssertAtLeastOneCanBeIncluded(relationshipsFound, relationshipName, position);
137-
138-
return children.AsReadOnly();
139-
}
140-
141-
private static HashSet<RelationshipAttribute> GetRelationshipsInConcreteTypes(ResourceType resourceType, string relationshipName)
142-
{
143-
HashSet<RelationshipAttribute> relationshipsToInclude = [];
144-
145-
foreach (RelationshipAttribute relationship in resourceType.GetRelationshipsInTypeOrDerived(relationshipName))
146-
{
147-
if (!relationship.LeftType.ClrType.IsAbstract)
148-
{
149-
relationshipsToInclude.Add(relationship);
150-
}
151-
152-
IncludeRelationshipsFromConcreteDerivedTypes(relationship, relationshipsToInclude);
153-
}
154-
155-
return relationshipsToInclude;
156-
}
157-
158-
private static void IncludeRelationshipsFromConcreteDerivedTypes(RelationshipAttribute relationship, HashSet<RelationshipAttribute> relationshipsToInclude)
159-
{
160-
foreach (ResourceType derivedType in relationship.LeftType.GetAllConcreteDerivedTypes())
161-
{
162-
RelationshipAttribute relationshipInDerived = derivedType.GetRelationshipByPublicName(relationship.PublicName);
163-
relationshipsToInclude.Add(relationshipInDerived);
164-
}
165-
}
166-
167-
private static void AssertRelationshipsFound(HashSet<RelationshipAttribute> relationshipsFound, string relationshipName,
168-
IReadOnlyCollection<IncludeTreeNode> parents, int position)
169-
{
170-
if (relationshipsFound.Count > 0)
171-
{
172-
return;
173-
}
174-
175-
ResourceType[] parentResourceTypes = parents.Select(parent => parent.Relationship.RightType).Distinct().ToArray();
176-
177-
bool hasDerivedTypes = parents.Any(parent => parent.Relationship.RightType.DirectlyDerivedTypes.Count > 0);
178-
179-
string message = GetErrorMessageForNoneFound(relationshipName, parentResourceTypes, hasDerivedTypes);
180-
throw new QueryParseException(message, position);
181-
}
182-
183-
private static string GetErrorMessageForNoneFound(string relationshipName, ResourceType[] parentResourceTypes, bool hasDerivedTypes)
184-
{
185-
var builder = new StringBuilder($"Relationship '{relationshipName}'");
186-
187-
if (parentResourceTypes.Length == 1)
188-
{
189-
builder.Append($" does not exist on resource type '{parentResourceTypes.First().PublicName}'");
190-
}
191-
else
192-
{
193-
string typeNames = string.Join(", ", parentResourceTypes.Select(type => $"'{type.PublicName}'"));
194-
builder.Append($" does not exist on any of the resource types {typeNames}");
195-
}
196-
197-
builder.Append(hasDerivedTypes ? " or any of its derived types." : ".");
198-
199-
return builder.ToString();
200-
}
201-
202-
private void AssertAtLeastOneCanBeIncluded(HashSet<RelationshipAttribute> relationshipsFound, string relationshipName, int position)
203-
{
204-
if (relationshipsFound.All(relationship => relationship.IsIncludeBlocked()))
205-
{
206-
ResourceType resourceType = relationshipsFound.First().LeftType;
207-
string message = $"Including the relationship '{relationshipName}' on '{resourceType}' is not allowed.";
208-
209-
var exception = new QueryParseException(message, position);
210-
string specificMessage = exception.GetMessageWithPosition(Source);
211-
212-
throw new InvalidQueryStringParameterException("include", "The specified include is invalid.", specificMessage);
213-
}
214-
}
215-
21660
private void ValidateMaximumIncludeDepth(IncludeExpression include, int position)
21761
{
21862
if (_options.MaximumIncludeDepth != null)
@@ -245,71 +89,4 @@ private static void ThrowIfMaximumDepthExceeded(IncludeElementExpression include
24589

24690
parentChain.Pop();
24791
}
248-
249-
private sealed class IncludeTreeNode
250-
{
251-
private readonly Dictionary<RelationshipAttribute, IncludeTreeNode> _children = [];
252-
253-
public RelationshipAttribute Relationship { get; }
254-
255-
private IncludeTreeNode(RelationshipAttribute relationship)
256-
{
257-
Relationship = relationship;
258-
}
259-
260-
public static IncludeTreeNode CreateRoot(ResourceType resourceType)
261-
{
262-
var relationship = new HiddenRootRelationshipAttribute(resourceType);
263-
return new IncludeTreeNode(relationship);
264-
}
265-
266-
public ReadOnlyCollection<IncludeTreeNode> EnsureChildren(RelationshipAttribute[] relationships)
267-
{
268-
foreach (RelationshipAttribute relationship in relationships)
269-
{
270-
if (!_children.ContainsKey(relationship))
271-
{
272-
var newChild = new IncludeTreeNode(relationship);
273-
_children.Add(relationship, newChild);
274-
}
275-
}
276-
277-
return _children.Where(pair => relationships.Contains(pair.Key)).Select(pair => pair.Value).ToArray().AsReadOnly();
278-
}
279-
280-
public IncludeExpression ToExpression()
281-
{
282-
IncludeElementExpression element = ToElementExpression();
283-
284-
if (element.Relationship is HiddenRootRelationshipAttribute)
285-
{
286-
return element.Children.Count > 0 ? new IncludeExpression(element.Children) : IncludeExpression.Empty;
287-
}
288-
289-
return new IncludeExpression(ImmutableHashSet.Create(element));
290-
}
291-
292-
private IncludeElementExpression ToElementExpression()
293-
{
294-
IImmutableSet<IncludeElementExpression> elementChildren = _children.Values.Select(child => child.ToElementExpression()).ToImmutableHashSet();
295-
return new IncludeElementExpression(Relationship, elementChildren);
296-
}
297-
298-
public override string ToString()
299-
{
300-
IncludeExpression include = ToExpression();
301-
return include.ToFullString();
302-
}
303-
304-
private sealed class HiddenRootRelationshipAttribute : RelationshipAttribute
305-
{
306-
public HiddenRootRelationshipAttribute(ResourceType rightType)
307-
{
308-
ArgumentNullException.ThrowIfNull(rightType);
309-
310-
RightType = rightType;
311-
PublicName = "<<root>>";
312-
}
313-
}
314-
}
31592
}

0 commit comments

Comments
 (0)