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,14 +14,18 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing;
13
14
public class FilterParser : QueryExpressionParser
14
15
{
15
16
private readonly IResourceFactory _resourceFactory ;
17
+ private readonly IEnumerable < IFilterValueConverter > _filterValueConverters ;
16
18
private readonly Action < ResourceFieldAttribute , ResourceType , string > ? _validateSingleFieldCallback ;
17
19
private ResourceType ? _resourceTypeInScope ;
18
20
19
- public FilterParser ( IResourceFactory resourceFactory , Action < ResourceFieldAttribute , ResourceType , string > ? validateSingleFieldCallback = null )
21
+ public FilterParser ( IResourceFactory resourceFactory , IEnumerable < IFilterValueConverter > filterValueConverters ,
22
+ Action < ResourceFieldAttribute , ResourceType , string > ? validateSingleFieldCallback = null )
20
23
{
21
24
ArgumentGuard . NotNull ( resourceFactory ) ;
25
+ ArgumentGuard . NotNull ( filterValueConverters ) ;
22
26
23
27
_resourceFactory = resourceFactory ;
28
+ _filterValueConverters = filterValueConverters ;
24
29
_validateSingleFieldCallback = validateSingleFieldCallback ;
25
30
}
26
31
@@ -153,7 +158,7 @@ protected ComparisonExpression ParseComparison(string operatorName)
153
158
}
154
159
else if ( leftTerm is ResourceFieldChainExpression fieldChain && fieldChain . Fields [ ^ 1 ] is AttrAttribute attribute )
155
160
{
156
- Converter < string , object > rightConstantValueConverter = GetConstantValueConverterForAttribute ( attribute ) ;
161
+ Converter < string , object > rightConstantValueConverter = GetConstantValueConverterForAttribute ( attribute , typeof ( ComparisonExpression ) ) ;
157
162
rightTerm = ParseCountOrConstantOrNullOrField ( FieldChainRequirements . EndsInAttribute , rightConstantValueConverter ) ;
158
163
}
159
164
else
@@ -172,16 +177,11 @@ protected MatchTextExpression ParseTextMatch(string matchFunctionName)
172
177
EatSingleCharacterToken ( TokenKind . OpenParen ) ;
173
178
174
179
ResourceFieldChainExpression targetAttributeChain = ParseFieldChain ( FieldChainRequirements . EndsInAttribute , null ) ;
175
- Type targetAttributeType = ( ( AttrAttribute ) targetAttributeChain . Fields [ ^ 1 ] ) . Property . PropertyType ;
176
-
177
- if ( targetAttributeType != typeof ( string ) )
178
- {
179
- throw new QueryParseException ( "Attribute of type 'String' expected." ) ;
180
- }
180
+ var targetAttribute = ( AttrAttribute ) targetAttributeChain . Fields [ ^ 1 ] ;
181
181
182
182
EatSingleCharacterToken ( TokenKind . Comma ) ;
183
183
184
- Converter < string , object > constantValueConverter = stringValue => stringValue ;
184
+ Converter < string , object > constantValueConverter = GetConstantValueConverterForAttribute ( targetAttribute , typeof ( MatchTextExpression ) ) ;
185
185
LiteralConstantExpression constant = ParseConstant ( constantValueConverter ) ;
186
186
187
187
EatSingleCharacterToken ( TokenKind . CloseParen ) ;
@@ -197,12 +197,12 @@ protected AnyExpression ParseAny()
197
197
198
198
ResourceFieldChainExpression targetAttributeChain = ParseFieldChain ( FieldChainRequirements . EndsInAttribute , null ) ;
199
199
var targetAttribute = ( AttrAttribute ) targetAttributeChain . Fields [ ^ 1 ] ;
200
- Converter < string , object > constantValueConverter = GetConstantValueConverterForAttribute ( targetAttribute ) ;
201
200
202
201
EatSingleCharacterToken ( TokenKind . Comma ) ;
203
202
204
203
ImmutableHashSet < LiteralConstantExpression > . Builder constantsBuilder = ImmutableHashSet . CreateBuilder < LiteralConstantExpression > ( ) ;
205
204
205
+ Converter < string , object > constantValueConverter = GetConstantValueConverterForAttribute ( targetAttribute , typeof ( AnyExpression ) ) ;
206
206
LiteralConstantExpression constant = ParseConstant ( constantValueConverter ) ;
207
207
constantsBuilder . Add ( constant ) ;
208
208
@@ -444,23 +444,76 @@ private Converter<string, object> GetConstantValueConverterForCount()
444
444
return stringValue => ConvertStringToType ( stringValue , typeof ( int ) ) ;
445
445
}
446
446
447
- private object ConvertStringToType ( string value , Type type )
447
+ private static object ConvertStringToType ( string value , Type type )
448
448
{
449
449
try
450
450
{
451
451
return RuntimeTypeConverter . ConvertType ( value , type ) ! ;
452
452
}
453
- catch ( FormatException )
453
+ catch ( FormatException exception )
454
454
{
455
- throw new QueryParseException ( $ "Failed to convert '{ value } ' of type 'String' to type '{ type . Name } '.") ;
455
+ throw new QueryParseException ( $ "Failed to convert '{ value } ' of type 'String' to type '{ type . Name } '.", exception ) ;
456
456
}
457
457
}
458
458
459
- private Converter < string , object > GetConstantValueConverterForAttribute ( AttrAttribute attribute )
459
+ private Converter < string , object > GetConstantValueConverterForAttribute ( AttrAttribute attribute , Type outerExpressionType )
460
460
{
461
- return stringValue => attribute . Property . Name == nameof ( Identifiable < object > . Id )
462
- ? DeObfuscateStringId ( attribute . Type . ClrType , stringValue )
463
- : ConvertStringToType ( stringValue , attribute . Property . PropertyType ) ;
461
+ return stringValue =>
462
+ {
463
+ object ? value = TryConvertFromStringUsingFilterValueConverters ( attribute , stringValue , outerExpressionType ) ;
464
+
465
+ if ( value != null )
466
+ {
467
+ return value ;
468
+ }
469
+
470
+ if ( outerExpressionType == typeof ( MatchTextExpression ) )
471
+ {
472
+ if ( attribute . Property . PropertyType != typeof ( string ) )
473
+ {
474
+ throw new QueryParseException ( "Attribute of type 'String' expected." ) ;
475
+ }
476
+ }
477
+ else
478
+ {
479
+ // Partial text matching on an obfuscated ID usually fails.
480
+ if ( attribute . Property . Name == nameof ( Identifiable < object > . Id ) )
481
+ {
482
+ return DeObfuscateStringId ( attribute . Type . ClrType , stringValue ) ;
483
+ }
484
+ }
485
+
486
+ return ConvertStringToType ( stringValue , attribute . Property . PropertyType ) ;
487
+ } ;
488
+ }
489
+
490
+ private object ? TryConvertFromStringUsingFilterValueConverters ( AttrAttribute attribute , string stringValue , Type outerExpressionType )
491
+ {
492
+ bool isPartialTextMatch = outerExpressionType == typeof ( MatchTextExpression ) ;
493
+
494
+ foreach ( IFilterValueConverter converter in _filterValueConverters )
495
+ {
496
+ if ( converter . CanConvert ( attribute ) )
497
+ {
498
+ object result = converter . Convert ( attribute , stringValue , outerExpressionType ) ;
499
+
500
+ if ( result == null )
501
+ {
502
+ throw new InvalidOperationException (
503
+ $ "Converter '{ converter . GetType ( ) . Name } ' returned null for '{ stringValue } ' on attribute '{ attribute . PublicName } '. Return a sentinel value instead.") ;
504
+ }
505
+
506
+ if ( isPartialTextMatch && result is not string )
507
+ {
508
+ throw new InvalidOperationException (
509
+ $ "Converter '{ converter . GetType ( ) . Name } ' returned a non-string value for '{ stringValue } ' on attribute '{ attribute . PublicName } '.") ;
510
+ }
511
+
512
+ return result ;
513
+ }
514
+ }
515
+
516
+ return null ;
464
517
}
465
518
466
519
private object DeObfuscateStringId ( Type resourceClrType , string stringId )
0 commit comments