3
3
using JetBrains . Annotations ;
4
4
using JsonApiDotNetCore . Configuration ;
5
5
using JsonApiDotNetCore . Queries . Expressions ;
6
+ using JsonApiDotNetCore . QueryStrings ;
6
7
using JsonApiDotNetCore . Resources ;
7
8
using JsonApiDotNetCore . Resources . Annotations ;
8
9
using JsonApiDotNetCore . Resources . Internal ;
@@ -13,15 +14,16 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing;
13
14
public class FilterParser : QueryExpressionParser
14
15
{
15
16
private readonly IResourceFactory _resourceFactory ;
16
- private readonly Action < ResourceFieldAttribute , ResourceType , string > ? _validateSingleFieldCallback ;
17
+ private readonly IEnumerable < IFilterValueConverter > _filterValueConverters ;
17
18
private ResourceType ? _resourceTypeInScope ;
18
19
19
- public FilterParser ( IResourceFactory resourceFactory , Action < ResourceFieldAttribute , ResourceType , string > ? validateSingleFieldCallback = null )
20
+ public FilterParser ( IResourceFactory resourceFactory , IEnumerable < IFilterValueConverter > filterValueConverters )
20
21
{
21
22
ArgumentGuard . NotNull ( resourceFactory ) ;
23
+ ArgumentGuard . NotNull ( filterValueConverters ) ;
22
24
23
25
_resourceFactory = resourceFactory ;
24
- _validateSingleFieldCallback = validateSingleFieldCallback ;
26
+ _filterValueConverters = filterValueConverters ;
25
27
}
26
28
27
29
public FilterExpression Parse ( string source , ResourceType resourceTypeInScope )
@@ -135,40 +137,34 @@ protected ComparisonExpression ParseComparison(string operatorName)
135
137
EatText ( operatorName ) ;
136
138
EatSingleCharacterToken ( TokenKind . OpenParen ) ;
137
139
138
- // Allow equality comparison of a HasOne relationship with null.
140
+ // Allow equality comparison of a to-one relationship with null.
139
141
FieldChainRequirements leftChainRequirements = comparisonOperator == ComparisonOperator . Equals
140
142
? FieldChainRequirements . EndsInAttribute | FieldChainRequirements . EndsInToOne
141
143
: FieldChainRequirements . EndsInAttribute ;
142
144
143
145
QueryExpression leftTerm = ParseCountOrField ( leftChainRequirements ) ;
144
- Converter < string , object > rightConstantValueConverter ;
146
+
147
+ EatSingleCharacterToken ( TokenKind . Comma ) ;
148
+
149
+ QueryExpression rightTerm ;
145
150
146
151
if ( leftTerm is CountExpression )
147
152
{
148
- rightConstantValueConverter = GetConstantValueConverterForCount ( ) ;
153
+ Converter < string , object > rightConstantValueConverter = GetConstantValueConverterForCount ( ) ;
154
+ rightTerm = ParseCountOrConstantOrField ( FieldChainRequirements . EndsInAttribute , rightConstantValueConverter ) ;
149
155
}
150
156
else if ( leftTerm is ResourceFieldChainExpression fieldChain && fieldChain . Fields [ ^ 1 ] is AttrAttribute attribute )
151
157
{
152
- rightConstantValueConverter = GetConstantValueConverterForAttribute ( attribute ) ;
158
+ Converter < string , object > rightConstantValueConverter = GetConstantValueConverterForAttribute ( attribute , typeof ( ComparisonExpression ) ) ;
159
+ rightTerm = ParseCountOrConstantOrNullOrField ( FieldChainRequirements . EndsInAttribute , rightConstantValueConverter ) ;
153
160
}
154
161
else
155
162
{
156
- // This temporary value never survives; it gets discarded during the second pass below.
157
- rightConstantValueConverter = _ => 0 ;
163
+ rightTerm = ParseNull ( ) ;
158
164
}
159
165
160
- EatSingleCharacterToken ( TokenKind . Comma ) ;
161
-
162
- QueryExpression rightTerm = ParseCountOrConstantOrNullOrField ( FieldChainRequirements . EndsInAttribute , rightConstantValueConverter ) ;
163
-
164
166
EatSingleCharacterToken ( TokenKind . CloseParen ) ;
165
167
166
- if ( leftTerm is ResourceFieldChainExpression leftChain && leftChain . Fields [ ^ 1 ] is RelationshipAttribute && rightTerm is not NullConstantExpression )
167
- {
168
- // Run another pass over left chain to produce an error.
169
- OnResolveFieldChain ( leftChain . ToString ( ) , FieldChainRequirements . EndsInAttribute ) ;
170
- }
171
-
172
168
return new ComparisonExpression ( comparisonOperator , leftTerm , rightTerm ) ;
173
169
}
174
170
@@ -178,16 +174,11 @@ protected MatchTextExpression ParseTextMatch(string matchFunctionName)
178
174
EatSingleCharacterToken ( TokenKind . OpenParen ) ;
179
175
180
176
ResourceFieldChainExpression targetAttributeChain = ParseFieldChain ( FieldChainRequirements . EndsInAttribute , null ) ;
181
- Type targetAttributeType = ( ( AttrAttribute ) targetAttributeChain . Fields [ ^ 1 ] ) . Property . PropertyType ;
182
-
183
- if ( targetAttributeType != typeof ( string ) )
184
- {
185
- throw new QueryParseException ( "Attribute of type 'String' expected." ) ;
186
- }
177
+ var targetAttribute = ( AttrAttribute ) targetAttributeChain . Fields [ ^ 1 ] ;
187
178
188
179
EatSingleCharacterToken ( TokenKind . Comma ) ;
189
180
190
- Converter < string , object > constantValueConverter = stringValue => stringValue ;
181
+ Converter < string , object > constantValueConverter = GetConstantValueConverterForAttribute ( targetAttribute , typeof ( MatchTextExpression ) ) ;
191
182
LiteralConstantExpression constant = ParseConstant ( constantValueConverter ) ;
192
183
193
184
EatSingleCharacterToken ( TokenKind . CloseParen ) ;
@@ -201,13 +192,14 @@ protected AnyExpression ParseAny()
201
192
EatText ( Keywords . Any ) ;
202
193
EatSingleCharacterToken ( TokenKind . OpenParen ) ;
203
194
204
- ResourceFieldChainExpression targetAttribute = ParseFieldChain ( FieldChainRequirements . EndsInAttribute , null ) ;
205
- Converter < string , object > constantValueConverter = GetConstantValueConverterForAttribute ( ( AttrAttribute ) targetAttribute . Fields [ ^ 1 ] ) ;
195
+ ResourceFieldChainExpression targetAttributeChain = ParseFieldChain ( FieldChainRequirements . EndsInAttribute , null ) ;
196
+ var targetAttribute = ( AttrAttribute ) targetAttributeChain . Fields [ ^ 1 ] ;
206
197
207
198
EatSingleCharacterToken ( TokenKind . Comma ) ;
208
199
209
200
ImmutableHashSet < LiteralConstantExpression > . Builder constantsBuilder = ImmutableHashSet . CreateBuilder < LiteralConstantExpression > ( ) ;
210
201
202
+ Converter < string , object > constantValueConverter = GetConstantValueConverterForAttribute ( targetAttribute , typeof ( AnyExpression ) ) ;
211
203
LiteralConstantExpression constant = ParseConstant ( constantValueConverter ) ;
212
204
constantsBuilder . Add ( constant ) ;
213
205
@@ -223,7 +215,7 @@ protected AnyExpression ParseAny()
223
215
224
216
IImmutableSet < LiteralConstantExpression > constantSet = constantsBuilder . ToImmutable ( ) ;
225
217
226
- return new AnyExpression ( targetAttribute , constantSet ) ;
218
+ return new AnyExpression ( targetAttributeChain , constantSet ) ;
227
219
}
228
220
229
221
protected HasExpression ParseHas ( )
@@ -349,6 +341,25 @@ protected QueryExpression ParseCountOrField(FieldChainRequirements chainRequirem
349
341
return ParseFieldChain ( chainRequirements , "Count function or field name expected." ) ;
350
342
}
351
343
344
+ protected QueryExpression ParseCountOrConstantOrField ( FieldChainRequirements chainRequirements , Converter < string , object > constantValueConverter )
345
+ {
346
+ CountExpression ? count = TryParseCount ( ) ;
347
+
348
+ if ( count != null )
349
+ {
350
+ return count ;
351
+ }
352
+
353
+ LiteralConstantExpression ? constant = TryParseConstant ( constantValueConverter ) ;
354
+
355
+ if ( constant != null )
356
+ {
357
+ return constant ;
358
+ }
359
+
360
+ return ParseFieldChain ( chainRequirements , "Count function, value between quotes or field name expected." ) ;
361
+ }
362
+
352
363
protected QueryExpression ParseCountOrConstantOrNullOrField ( FieldChainRequirements chainRequirements , Converter < string , object > constantValueConverter )
353
364
{
354
365
CountExpression ? count = TryParseCount ( ) ;
@@ -368,6 +379,19 @@ protected QueryExpression ParseCountOrConstantOrNullOrField(FieldChainRequiremen
368
379
return ParseFieldChain ( chainRequirements , "Count function, value between quotes, null or field name expected." ) ;
369
380
}
370
381
382
+ protected LiteralConstantExpression ? TryParseConstant ( Converter < string , object > constantValueConverter )
383
+ {
384
+ if ( TokenStack . TryPeek ( out Token ? nextToken ) && nextToken . Kind == TokenKind . QuotedText )
385
+ {
386
+ TokenStack . Pop ( ) ;
387
+
388
+ object constantValue = constantValueConverter ( nextToken . Value ! ) ;
389
+ return new LiteralConstantExpression ( constantValue , nextToken . Value ! ) ;
390
+ }
391
+
392
+ return null ;
393
+ }
394
+
371
395
protected IdentifierExpression ? TryParseConstantOrNull ( Converter < string , object > constantValueConverter )
372
396
{
373
397
if ( TokenStack . TryPeek ( out Token ? nextToken ) )
@@ -392,37 +416,93 @@ protected QueryExpression ParseCountOrConstantOrNullOrField(FieldChainRequiremen
392
416
393
417
protected LiteralConstantExpression ParseConstant ( Converter < string , object > constantValueConverter )
394
418
{
395
- if ( TokenStack . TryPop ( out Token ? token ) && token . Kind == TokenKind . QuotedText )
419
+ LiteralConstantExpression ? constant = TryParseConstant ( constantValueConverter ) ;
420
+
421
+ if ( constant == null )
422
+ {
423
+ throw new QueryParseException ( "Value between quotes expected." ) ;
424
+ }
425
+
426
+ return constant ;
427
+ }
428
+
429
+ protected NullConstantExpression ParseNull ( )
430
+ {
431
+ if ( TokenStack . TryPop ( out Token ? token ) && token is { Kind : TokenKind . Text , Value : Keywords . Null } )
396
432
{
397
- object constantValue = constantValueConverter ( token . Value ! ) ;
398
- return new LiteralConstantExpression ( constantValue , token . Value ! ) ;
433
+ return NullConstantExpression . Instance ;
399
434
}
400
435
401
- throw new QueryParseException ( "Value between quotes expected." ) ;
436
+ throw new QueryParseException ( "null expected." ) ;
402
437
}
403
438
404
439
private Converter < string , object > GetConstantValueConverterForCount ( )
405
440
{
406
441
return stringValue => ConvertStringToType ( stringValue , typeof ( int ) ) ;
407
442
}
408
443
409
- private object ConvertStringToType ( string value , Type type )
444
+ private static object ConvertStringToType ( string value , Type type )
410
445
{
411
446
try
412
447
{
413
448
return RuntimeTypeConverter . ConvertType ( value , type ) ! ;
414
449
}
415
- catch ( FormatException )
450
+ catch ( FormatException exception )
416
451
{
417
- throw new QueryParseException ( $ "Failed to convert '{ value } ' of type 'String' to type '{ type . Name } '.") ;
452
+ throw new QueryParseException ( $ "Failed to convert '{ value } ' of type 'String' to type '{ type . Name } '.", exception ) ;
418
453
}
419
454
}
420
455
421
- private Converter < string , object > GetConstantValueConverterForAttribute ( AttrAttribute attribute )
456
+ private Converter < string , object > GetConstantValueConverterForAttribute ( AttrAttribute attribute , Type outerExpressionType )
422
457
{
423
- return stringValue => attribute . Property . Name == nameof ( Identifiable < object > . Id )
424
- ? DeObfuscateStringId ( attribute . Type . ClrType , stringValue )
425
- : ConvertStringToType ( stringValue , attribute . Property . PropertyType ) ;
458
+ return stringValue =>
459
+ {
460
+ object ? value = TryConvertFromStringUsingFilterValueConverters ( attribute , stringValue , outerExpressionType ) ;
461
+
462
+ if ( value != null )
463
+ {
464
+ return value ;
465
+ }
466
+
467
+ if ( outerExpressionType == typeof ( MatchTextExpression ) )
468
+ {
469
+ if ( attribute . Property . PropertyType != typeof ( string ) )
470
+ {
471
+ throw new QueryParseException ( "Attribute of type 'String' expected." ) ;
472
+ }
473
+ }
474
+ else
475
+ {
476
+ // Partial text matching on an obfuscated ID usually fails.
477
+ if ( attribute . Property . Name == nameof ( Identifiable < object > . Id ) )
478
+ {
479
+ return DeObfuscateStringId ( attribute . Type . ClrType , stringValue ) ;
480
+ }
481
+ }
482
+
483
+ return ConvertStringToType ( stringValue , attribute . Property . PropertyType ) ;
484
+ } ;
485
+ }
486
+
487
+ private object ? TryConvertFromStringUsingFilterValueConverters ( AttrAttribute attribute , string stringValue , Type outerExpressionType )
488
+ {
489
+ foreach ( IFilterValueConverter converter in _filterValueConverters )
490
+ {
491
+ if ( converter . CanConvert ( attribute ) )
492
+ {
493
+ object result = converter . Convert ( attribute , stringValue , outerExpressionType ) ;
494
+
495
+ if ( result == null )
496
+ {
497
+ throw new InvalidOperationException (
498
+ $ "Converter '{ converter . GetType ( ) . Name } ' returned null for '{ stringValue } ' on attribute '{ attribute . PublicName } '. Return a sentinel value instead.") ;
499
+ }
500
+
501
+ return result ;
502
+ }
503
+ }
504
+
505
+ return null ;
426
506
}
427
507
428
508
private object DeObfuscateStringId ( Type resourceClrType , string stringId )
@@ -436,29 +516,37 @@ protected override IImmutableList<ResourceFieldAttribute> OnResolveFieldChain(st
436
516
{
437
517
if ( chainRequirements == FieldChainRequirements . EndsInToMany )
438
518
{
439
- return ChainResolver . ResolveToOneChainEndingInToMany ( _resourceTypeInScope ! , path , FieldChainInheritanceRequirement . Disabled ,
440
- _validateSingleFieldCallback ) ;
519
+ return ChainResolver . ResolveToOneChainEndingInToMany ( _resourceTypeInScope ! , path , FieldChainInheritanceRequirement . Disabled , ValidateSingleField ) ;
441
520
}
442
521
443
522
if ( chainRequirements == FieldChainRequirements . EndsInAttribute )
444
523
{
445
524
return ChainResolver . ResolveToOneChainEndingInAttribute ( _resourceTypeInScope ! , path , FieldChainInheritanceRequirement . Disabled ,
446
- _validateSingleFieldCallback ) ;
525
+ ValidateSingleField ) ;
447
526
}
448
527
449
528
if ( chainRequirements == FieldChainRequirements . EndsInToOne )
450
529
{
451
- return ChainResolver . ResolveToOneChain ( _resourceTypeInScope ! , path , _validateSingleFieldCallback ) ;
530
+ return ChainResolver . ResolveToOneChain ( _resourceTypeInScope ! , path , ValidateSingleField ) ;
452
531
}
453
532
454
533
if ( chainRequirements . HasFlag ( FieldChainRequirements . EndsInAttribute ) && chainRequirements . HasFlag ( FieldChainRequirements . EndsInToOne ) )
455
534
{
456
- return ChainResolver . ResolveToOneChainEndingInAttributeOrToOne ( _resourceTypeInScope ! , path , _validateSingleFieldCallback ) ;
535
+ return ChainResolver . ResolveToOneChainEndingInAttributeOrToOne ( _resourceTypeInScope ! , path , ValidateSingleField ) ;
457
536
}
458
537
459
538
throw new InvalidOperationException ( $ "Unexpected combination of chain requirement flags '{ chainRequirements } '.") ;
460
539
}
461
540
541
+ protected override void ValidateSingleField ( ResourceFieldAttribute field , ResourceType resourceType , string path )
542
+ {
543
+ if ( field . IsFilterBlocked ( ) )
544
+ {
545
+ string kind = field is AttrAttribute ? "attribute" : "relationship" ;
546
+ throw new QueryParseException ( $ "Filtering on { kind } '{ field . PublicName } ' is not allowed.") ;
547
+ }
548
+ }
549
+
462
550
private TResult InScopeOfResourceType < TResult > ( ResourceType resourceType , Func < TResult > action )
463
551
{
464
552
ResourceType ? backupType = _resourceTypeInScope ;
0 commit comments