1- using System . Collections . Immutable ;
2- using System . Collections . ObjectModel ;
3- using System . Text ;
41using JetBrains . Annotations ;
52using JsonApiDotNetCore . Configuration ;
6- using JsonApiDotNetCore . Errors ;
73using JsonApiDotNetCore . Queries . Expressions ;
84using JsonApiDotNetCore . Resources . Annotations ;
95
@@ -29,190 +25,17 @@ public IncludeExpression Parse(string source, ResourceType resourceType)
2925
3026 Tokenize ( source ) ;
3127
32- IncludeExpression expression = ParseInclude ( source , resourceType ) ;
28+ IncludeExpression expression = ParseInclude ( resourceType ) ;
3329
3430 AssertTokenStackIsEmpty ( ) ;
3531 ValidateMaximumIncludeDepth ( expression , 0 ) ;
3632
3733 return expression ;
3834 }
3935
40- protected virtual IncludeExpression ParseInclude ( string source , ResourceType resourceType )
36+ protected virtual IncludeExpression ParseInclude ( ResourceType resourceType )
4137 {
42- ArgumentNullException . ThrowIfNull ( source ) ;
43- ArgumentNullException . ThrowIfNull ( resourceType ) ;
44-
45- var treeRoot = IncludeTreeNode . CreateRoot ( resourceType ) ;
46- bool isAtStart = true ;
47-
48- while ( TokenStack . Count > 0 )
49- {
50- if ( ! isAtStart )
51- {
52- EatSingleCharacterToken ( TokenKind . Comma ) ;
53- }
54- else
55- {
56- isAtStart = false ;
57- }
58-
59- ParseRelationshipChain ( source , treeRoot ) ;
60- }
61-
62- return treeRoot . ToExpression ( ) ;
63- }
64-
65- private void ParseRelationshipChain ( string source , IncludeTreeNode treeRoot )
66- {
67- // A relationship name usually matches a single relationship, even when overridden in derived types.
68- // But in the following case, two relationships are matched on GET /shoppingBaskets?include=items:
69- //
70- // public abstract class ShoppingBasket : Identifiable<long>
71- // {
72- // }
73- //
74- // public sealed class SilverShoppingBasket : ShoppingBasket
75- // {
76- // [HasMany]
77- // public ISet<Article> Items { get; get; }
78- // }
79- //
80- // public sealed class PlatinumShoppingBasket : ShoppingBasket
81- // {
82- // [HasMany]
83- // public ISet<Product> Items { get; get; }
84- // }
85- //
86- // Now if the include chain has subsequent relationships, we need to scan both Items relationships for matches,
87- // which is why ParseRelationshipName returns a collection.
88- //
89- // The advantage of this unfolding is we don't require callers to upcast in relationship chains. The downside is
90- // that there's currently no way to include Products without Articles. We could add such optional upcast syntax
91- // in the future, if desired.
92-
93- ReadOnlyCollection < IncludeTreeNode > children = ParseRelationshipName ( source , [ treeRoot ] ) ;
94-
95- while ( TokenStack . TryPeek ( out Token ? nextToken ) && nextToken . Kind == TokenKind . Period )
96- {
97- EatSingleCharacterToken ( TokenKind . Period ) ;
98-
99- children = ParseRelationshipName ( source , children ) ;
100- }
101- }
102-
103- private ReadOnlyCollection < IncludeTreeNode > ParseRelationshipName ( string source , IReadOnlyCollection < IncludeTreeNode > parents )
104- {
105- int position = GetNextTokenPositionOrEnd ( ) ;
106-
107- if ( TokenStack . TryPop ( out Token ? token ) && token . Kind == TokenKind . Text )
108- {
109- return LookupRelationshipName ( token . Value ! , parents , source , position ) ;
110- }
111-
112- throw new QueryParseException ( "Relationship name expected." , position ) ;
113- }
114-
115- private static ReadOnlyCollection < IncludeTreeNode > LookupRelationshipName ( string relationshipName , IReadOnlyCollection < IncludeTreeNode > parents ,
116- string source , int position )
117- {
118- List < IncludeTreeNode > children = [ ] ;
119- HashSet < RelationshipAttribute > relationshipsFound = [ ] ;
120-
121- foreach ( IncludeTreeNode parent in parents )
122- {
123- // Depending on the left side of the include chain, we may match relationships anywhere in the resource type hierarchy.
124- // This is compensated for when rendering the response, which substitutes relationships on base types with the derived ones.
125- HashSet < RelationshipAttribute > relationships = GetRelationshipsInConcreteTypes ( parent . Relationship . RightType , relationshipName ) ;
126-
127- if ( relationships . Count > 0 )
128- {
129- relationshipsFound . UnionWith ( relationships ) ;
130-
131- RelationshipAttribute [ ] relationshipsToInclude = relationships . Where ( relationship => ! relationship . IsIncludeBlocked ( ) ) . ToArray ( ) ;
132- ReadOnlyCollection < IncludeTreeNode > affectedChildren = parent . EnsureChildren ( relationshipsToInclude ) ;
133- children . AddRange ( affectedChildren ) ;
134- }
135- }
136-
137- AssertRelationshipsFound ( relationshipsFound , relationshipName , parents , position ) ;
138- AssertAtLeastOneCanBeIncluded ( relationshipsFound , relationshipName , source , position ) ;
139-
140- return children . AsReadOnly ( ) ;
141- }
142-
143- private static HashSet < RelationshipAttribute > GetRelationshipsInConcreteTypes ( ResourceType resourceType , string relationshipName )
144- {
145- HashSet < RelationshipAttribute > relationshipsToInclude = [ ] ;
146-
147- foreach ( RelationshipAttribute relationship in resourceType . GetRelationshipsInTypeOrDerived ( relationshipName ) )
148- {
149- if ( ! relationship . LeftType . ClrType . IsAbstract )
150- {
151- relationshipsToInclude . Add ( relationship ) ;
152- }
153-
154- IncludeRelationshipsFromConcreteDerivedTypes ( relationship , relationshipsToInclude ) ;
155- }
156-
157- return relationshipsToInclude ;
158- }
159-
160- private static void IncludeRelationshipsFromConcreteDerivedTypes ( RelationshipAttribute relationship , HashSet < RelationshipAttribute > relationshipsToInclude )
161- {
162- foreach ( ResourceType derivedType in relationship . LeftType . GetAllConcreteDerivedTypes ( ) )
163- {
164- RelationshipAttribute relationshipInDerived = derivedType . GetRelationshipByPublicName ( relationship . PublicName ) ;
165- relationshipsToInclude . Add ( relationshipInDerived ) ;
166- }
167- }
168-
169- private static void AssertRelationshipsFound ( HashSet < RelationshipAttribute > relationshipsFound , string relationshipName ,
170- IReadOnlyCollection < IncludeTreeNode > parents , int position )
171- {
172- if ( relationshipsFound . Count > 0 )
173- {
174- return ;
175- }
176-
177- ResourceType [ ] parentResourceTypes = parents . Select ( parent => parent . Relationship . RightType ) . Distinct ( ) . ToArray ( ) ;
178-
179- bool hasDerivedTypes = parents . Any ( parent => parent . Relationship . RightType . DirectlyDerivedTypes . Count > 0 ) ;
180-
181- string message = GetErrorMessageForNoneFound ( relationshipName , parentResourceTypes , hasDerivedTypes ) ;
182- throw new QueryParseException ( message , position ) ;
183- }
184-
185- private static string GetErrorMessageForNoneFound ( string relationshipName , ResourceType [ ] parentResourceTypes , bool hasDerivedTypes )
186- {
187- var builder = new StringBuilder ( $ "Relationship '{ relationshipName } '") ;
188-
189- if ( parentResourceTypes . Length == 1 )
190- {
191- builder . Append ( $ " does not exist on resource type '{ parentResourceTypes . First ( ) . PublicName } '") ;
192- }
193- else
194- {
195- string typeNames = string . Join ( ", " , parentResourceTypes . Select ( type => $ "'{ type . PublicName } '") ) ;
196- builder . Append ( $ " does not exist on any of the resource types { typeNames } ") ;
197- }
198-
199- builder . Append ( hasDerivedTypes ? " or any of its derived types." : "." ) ;
200-
201- return builder . ToString ( ) ;
202- }
203-
204- private static void AssertAtLeastOneCanBeIncluded ( HashSet < RelationshipAttribute > relationshipsFound , string relationshipName , string source , int position )
205- {
206- if ( relationshipsFound . All ( relationship => relationship . IsIncludeBlocked ( ) ) )
207- {
208- ResourceType resourceType = relationshipsFound . First ( ) . LeftType ;
209- string message = $ "Including the relationship '{ relationshipName } ' on '{ resourceType } ' is not allowed.";
210-
211- var exception = new QueryParseException ( message , position ) ;
212- string specificMessage = exception . GetMessageWithPosition ( source ) ;
213-
214- throw new InvalidQueryStringParameterException ( "include" , "The specified include is invalid." , specificMessage ) ;
215- }
38+ return ParseCommaSeparatedSequenceOfRelationshipChains ( resourceType ) ;
21639 }
21740
21841 private void ValidateMaximumIncludeDepth ( IncludeExpression include , int position )
@@ -247,71 +70,4 @@ private static void ThrowIfMaximumDepthExceeded(IncludeElementExpression include
24770
24871 parentChain . Pop ( ) ;
24972 }
250-
251- private sealed class IncludeTreeNode
252- {
253- private readonly Dictionary < RelationshipAttribute , IncludeTreeNode > _children = [ ] ;
254-
255- public RelationshipAttribute Relationship { get ; }
256-
257- private IncludeTreeNode ( RelationshipAttribute relationship )
258- {
259- Relationship = relationship ;
260- }
261-
262- public static IncludeTreeNode CreateRoot ( ResourceType resourceType )
263- {
264- var relationship = new HiddenRootRelationshipAttribute ( resourceType ) ;
265- return new IncludeTreeNode ( relationship ) ;
266- }
267-
268- public ReadOnlyCollection < IncludeTreeNode > EnsureChildren ( RelationshipAttribute [ ] relationships )
269- {
270- foreach ( RelationshipAttribute relationship in relationships )
271- {
272- if ( ! _children . ContainsKey ( relationship ) )
273- {
274- var newChild = new IncludeTreeNode ( relationship ) ;
275- _children . Add ( relationship , newChild ) ;
276- }
277- }
278-
279- return _children . Where ( pair => relationships . Contains ( pair . Key ) ) . Select ( pair => pair . Value ) . ToArray ( ) . AsReadOnly ( ) ;
280- }
281-
282- public IncludeExpression ToExpression ( )
283- {
284- IncludeElementExpression element = ToElementExpression ( ) ;
285-
286- if ( element . Relationship is HiddenRootRelationshipAttribute )
287- {
288- return element . Children . Count > 0 ? new IncludeExpression ( element . Children ) : IncludeExpression . Empty ;
289- }
290-
291- return new IncludeExpression ( ImmutableHashSet . Create ( element ) ) ;
292- }
293-
294- private IncludeElementExpression ToElementExpression ( )
295- {
296- IImmutableSet < IncludeElementExpression > elementChildren = _children . Values . Select ( child => child . ToElementExpression ( ) ) . ToImmutableHashSet ( ) ;
297- return new IncludeElementExpression ( Relationship , elementChildren ) ;
298- }
299-
300- public override string ToString ( )
301- {
302- IncludeExpression include = ToExpression ( ) ;
303- return include . ToFullString ( ) ;
304- }
305-
306- private sealed class HiddenRootRelationshipAttribute : RelationshipAttribute
307- {
308- public HiddenRootRelationshipAttribute ( ResourceType rightType )
309- {
310- ArgumentNullException . ThrowIfNull ( rightType ) ;
311-
312- RightType = rightType ;
313- PublicName = "<<root>>" ;
314- }
315- }
316- }
31773}
0 commit comments