1
- using System . Collections . Immutable ;
2
- using System . Collections . ObjectModel ;
3
- using System . Text ;
4
1
using JetBrains . Annotations ;
5
2
using JsonApiDotNetCore . Configuration ;
6
- using JsonApiDotNetCore . Errors ;
7
3
using JsonApiDotNetCore . Queries . Expressions ;
8
4
using JsonApiDotNetCore . Resources . Annotations ;
9
5
@@ -29,190 +25,17 @@ public IncludeExpression Parse(string source, ResourceType resourceType)
29
25
30
26
Tokenize ( source ) ;
31
27
32
- IncludeExpression expression = ParseInclude ( source , resourceType ) ;
28
+ IncludeExpression expression = ParseInclude ( resourceType ) ;
33
29
34
30
AssertTokenStackIsEmpty ( ) ;
35
31
ValidateMaximumIncludeDepth ( expression , 0 ) ;
36
32
37
33
return expression ;
38
34
}
39
35
40
- protected virtual IncludeExpression ParseInclude ( string source , ResourceType resourceType )
36
+ protected virtual IncludeExpression ParseInclude ( ResourceType resourceType )
41
37
{
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 ) ;
216
39
}
217
40
218
41
private void ValidateMaximumIncludeDepth ( IncludeExpression include , int position )
@@ -247,71 +70,4 @@ private static void ThrowIfMaximumDepthExceeded(IncludeElementExpression include
247
70
248
71
parentChain . Pop ( ) ;
249
72
}
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
- }
317
73
}
0 commit comments