Skip to content

Commit f1c9223

Browse files
committed
--wip-without-tests
1 parent f818c8f commit f1c9223

File tree

8 files changed

+127
-23
lines changed

8 files changed

+127
-23
lines changed

benchmarks/QueryString/QueryStringParserBenchmarks.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public QueryStringParserBenchmarks()
3838
var resourceFactory = new ResourceFactory(new ServiceContainer());
3939

4040
var includeReader = new IncludeQueryStringParameterReader(request, resourceGraph, options);
41-
var filterReader = new FilterQueryStringParameterReader(request, resourceGraph, resourceFactory, options);
41+
var filterReader = new FilterQueryStringParameterReader(request, resourceGraph, resourceFactory, options, Enumerable.Empty<IFilterValueConverter>());
4242
var sortReader = new SortQueryStringParameterReader(request, resourceGraph);
4343
var sparseFieldSetReader = new SparseFieldSetQueryStringParameterReader(request, resourceGraph);
4444
var paginationReader = new PaginationQueryStringParameterReader(request, resourceGraph, options);

src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using JetBrains.Annotations;
44
using JsonApiDotNetCore.Configuration;
55
using JsonApiDotNetCore.Queries.Expressions;
6+
using JsonApiDotNetCore.QueryStrings;
67
using JsonApiDotNetCore.Resources;
78
using JsonApiDotNetCore.Resources.Annotations;
89
using JsonApiDotNetCore.Resources.Internal;
@@ -13,14 +14,18 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing;
1314
public class FilterParser : QueryExpressionParser
1415
{
1516
private readonly IResourceFactory _resourceFactory;
17+
private readonly IEnumerable<IFilterValueConverter> _filterValueConverters;
1618
private readonly Action<ResourceFieldAttribute, ResourceType, string>? _validateSingleFieldCallback;
1719
private ResourceType? _resourceTypeInScope;
1820

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)
2023
{
2124
ArgumentGuard.NotNull(resourceFactory);
25+
ArgumentGuard.NotNull(filterValueConverters);
2226

2327
_resourceFactory = resourceFactory;
28+
_filterValueConverters = filterValueConverters;
2429
_validateSingleFieldCallback = validateSingleFieldCallback;
2530
}
2631

@@ -153,7 +158,7 @@ protected ComparisonExpression ParseComparison(string operatorName)
153158
}
154159
else if (leftTerm is ResourceFieldChainExpression fieldChain && fieldChain.Fields[^1] is AttrAttribute attribute)
155160
{
156-
Converter<string, object> rightConstantValueConverter = GetConstantValueConverterForAttribute(attribute);
161+
Converter<string, object> rightConstantValueConverter = GetConstantValueConverterForAttribute(attribute, typeof(ComparisonExpression));
157162
rightTerm = ParseCountOrConstantOrNullOrField(FieldChainRequirements.EndsInAttribute, rightConstantValueConverter);
158163
}
159164
else
@@ -172,16 +177,11 @@ protected MatchTextExpression ParseTextMatch(string matchFunctionName)
172177
EatSingleCharacterToken(TokenKind.OpenParen);
173178

174179
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];
181181

182182
EatSingleCharacterToken(TokenKind.Comma);
183183

184-
Converter<string, object> constantValueConverter = stringValue => stringValue;
184+
Converter<string, object> constantValueConverter = GetConstantValueConverterForAttribute(targetAttribute, typeof(MatchTextExpression));
185185
LiteralConstantExpression constant = ParseConstant(constantValueConverter);
186186

187187
EatSingleCharacterToken(TokenKind.CloseParen);
@@ -197,12 +197,12 @@ protected AnyExpression ParseAny()
197197

198198
ResourceFieldChainExpression targetAttributeChain = ParseFieldChain(FieldChainRequirements.EndsInAttribute, null);
199199
var targetAttribute = (AttrAttribute)targetAttributeChain.Fields[^1];
200-
Converter<string, object> constantValueConverter = GetConstantValueConverterForAttribute(targetAttribute);
201200

202201
EatSingleCharacterToken(TokenKind.Comma);
203202

204203
ImmutableHashSet<LiteralConstantExpression>.Builder constantsBuilder = ImmutableHashSet.CreateBuilder<LiteralConstantExpression>();
205204

205+
Converter<string, object> constantValueConverter = GetConstantValueConverterForAttribute(targetAttribute, typeof(AnyExpression));
206206
LiteralConstantExpression constant = ParseConstant(constantValueConverter);
207207
constantsBuilder.Add(constant);
208208

@@ -444,23 +444,76 @@ private Converter<string, object> GetConstantValueConverterForCount()
444444
return stringValue => ConvertStringToType(stringValue, typeof(int));
445445
}
446446

447-
private object ConvertStringToType(string value, Type type)
447+
private static object ConvertStringToType(string value, Type type)
448448
{
449449
try
450450
{
451451
return RuntimeTypeConverter.ConvertType(value, type)!;
452452
}
453-
catch (FormatException)
453+
catch (FormatException exception)
454454
{
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);
456456
}
457457
}
458458

459-
private Converter<string, object> GetConstantValueConverterForAttribute(AttrAttribute attribute)
459+
private Converter<string, object> GetConstantValueConverterForAttribute(AttrAttribute attribute, Type outerExpressionType)
460460
{
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;
464517
}
465518

466519
private object DeObfuscateStringId(Type resourceClrType, string stringId)

src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryParseException.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@ public QueryParseException(string message)
99
: base(message)
1010
{
1111
}
12+
13+
public QueryParseException(string message, Exception innerException)
14+
: base(message, innerException)
15+
{
16+
}
1217
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using JsonApiDotNetCore.Queries.Expressions;
2+
using JsonApiDotNetCore.Queries.Internal.Parsing;
3+
using JsonApiDotNetCore.Resources;
4+
using JsonApiDotNetCore.Resources.Annotations;
5+
6+
namespace JsonApiDotNetCore.QueryStrings;
7+
8+
/// <summary>
9+
/// Provides conversion of a single-quoted value that occurs in a filter function of a query string.
10+
/// </summary>
11+
public interface IFilterValueConverter
12+
{
13+
/// <summary>
14+
/// Indicates whether this converter can be used for the specified <see cref="AttrAttribute" />.
15+
/// </summary>
16+
/// <param name="attribute">
17+
/// The JSON:API attribute this conversion applies to.
18+
/// </param>
19+
bool CanConvert(AttrAttribute attribute);
20+
21+
/// <summary>
22+
/// Converts <paramref name="value" /> to the specified <paramref name="attribute" />.
23+
/// </summary>
24+
/// <param name="attribute">
25+
/// The JSON:API attribute this conversion applies to.
26+
/// </param>
27+
/// <param name="value">
28+
/// The literal text (without the surrounding single quotes) from the query string.
29+
/// </param>
30+
/// <param name="outerExpressionType">
31+
/// The filter function this conversion applies to, which can be <see cref="ComparisonExpression" />, <see cref="AnyExpression" /> or
32+
/// <see cref="MatchTextExpression" />. In case of <see cref="MatchTextExpression" />, a <see cref="string" /> must be returned.
33+
/// </param>
34+
/// <returns>
35+
/// The converted value. Must not be null. In case the type differs from the resource property type, use a
36+
/// <see cref="QueryExpressionRewriter{TArgument}" /> from <see cref="IResourceDefinition{TResource,TId}.OnApplyFilter" /> to produce a valid filter.
37+
/// </returns>
38+
/// <exception cref="QueryParseException">
39+
/// The conversion failed because <paramref name="value" /> is invalid.
40+
/// </exception>
41+
object Convert(AttrAttribute attribute, string value, Type outerExpressionType);
42+
}

src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,16 @@ public class FilterQueryStringParameterReader : QueryStringParameterReader, IFil
2828

2929
public bool AllowEmptyValue => false;
3030

31-
public FilterQueryStringParameterReader(IJsonApiRequest request, IResourceGraph resourceGraph, IResourceFactory resourceFactory, IJsonApiOptions options)
31+
public FilterQueryStringParameterReader(IJsonApiRequest request, IResourceGraph resourceGraph, IResourceFactory resourceFactory, IJsonApiOptions options,
32+
IEnumerable<IFilterValueConverter> filterValueConverters)
3233
: base(request, resourceGraph)
3334
{
3435
ArgumentGuard.NotNull(options);
36+
ArgumentGuard.NotNull(filterValueConverters);
3537

3638
_options = options;
3739
_scopeParser = new QueryStringParameterScopeParser(FieldChainRequirements.EndsInToMany);
38-
_filterParser = new FilterParser(resourceFactory, ValidateSingleField);
40+
_filterParser = new FilterParser(resourceFactory, filterValueConverters, ValidateSingleField);
3941
}
4042

4143
protected void ValidateSingleField(ResourceFieldAttribute field, ResourceType resourceType, string path)

test/JsonApiDotNetCoreTests/UnitTests/Queries/QueryExpressionRewriterTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using JsonApiDotNetCore.Configuration;
55
using JsonApiDotNetCore.Queries.Expressions;
66
using JsonApiDotNetCore.Queries.Internal.Parsing;
7+
using JsonApiDotNetCore.QueryStrings;
78
using JsonApiDotNetCore.Resources;
89
using JsonApiDotNetCoreTests.IntegrationTests.QueryStrings;
910
using Microsoft.Extensions.Logging.Abstractions;
@@ -123,7 +124,7 @@ public void VisitSparseFieldTable()
123124
public void VisitFilter(string expressionText, string expectedTypes)
124125
{
125126
// Arrange
126-
var parser = new FilterParser(ResourceFactory);
127+
var parser = new FilterParser(ResourceFactory, Enumerable.Empty<IFilterValueConverter>());
127128
ResourceType webAccountType = ResourceGraph.GetResourceType<WebAccount>();
128129

129130
QueryExpression expression = parser.Parse(expressionText, webAccountType);

test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/FilterParseTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public FilterParseTests()
2323
Options.EnableLegacyFilterNotation = false;
2424

2525
var resourceFactory = new ResourceFactory(new ServiceContainer());
26-
_reader = new FilterQueryStringParameterReader(Request, ResourceGraph, resourceFactory, Options);
26+
_reader = new FilterQueryStringParameterReader(Request, ResourceGraph, resourceFactory, Options, Enumerable.Empty<IFilterValueConverter>());
2727
}
2828

2929
[Theory]

test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/LegacyFilterParseTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using JsonApiDotNetCore.Errors;
55
using JsonApiDotNetCore.Queries;
66
using JsonApiDotNetCore.Queries.Expressions;
7+
using JsonApiDotNetCore.QueryStrings;
78
using JsonApiDotNetCore.QueryStrings.Internal;
89
using JsonApiDotNetCore.Resources;
910
using JsonApiDotNetCore.Serialization.Objects;
@@ -24,7 +25,7 @@ public LegacyFilterParseTests()
2425
Request.PrimaryResourceType = ResourceGraph.GetResourceType<BlogPost>();
2526

2627
var resourceFactory = new ResourceFactory(new ServiceContainer());
27-
_reader = new FilterQueryStringParameterReader(Request, ResourceGraph, resourceFactory, Options);
28+
_reader = new FilterQueryStringParameterReader(Request, ResourceGraph, resourceFactory, Options, Enumerable.Empty<IFilterValueConverter>());
2829
}
2930

3031
[Theory]

0 commit comments

Comments
 (0)