Skip to content

Commit 828c9db

Browse files
committed
feat: can support primitive lists
1 parent ed8a81d commit 828c9db

11 files changed

Lines changed: 149 additions & 5 deletions

File tree

QueryKit.UnitTests/FilterParserTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,5 +763,41 @@ public void collection_has_operator_greater_than_equal()
763763
filterExpression.ToString().Should()
764764
.Be(""""x => (x.Ingredients.Count() >= 0)"""");
765765
}
766+
767+
[Fact]
768+
public void primitive_collection_has()
769+
{
770+
var input = """Tags ^$ "winner" """;
771+
var filterExpression = FilterParser.ParseFilter<Recipe>(input);
772+
filterExpression.ToString().Should()
773+
.Be(""""x => x.Tags.Any(z => (z == "winner"))"""");
774+
}
775+
776+
[Fact]
777+
public void primitive_collection_does_not_have()
778+
{
779+
var input = """Tags !^$ "winner" """;
780+
var filterExpression = FilterParser.ParseFilter<Recipe>(input);
781+
filterExpression.ToString().Should()
782+
.Be(""""x => x.Tags.Any(z => (z != "winner"))"""");
783+
}
784+
785+
[Fact]
786+
public void primitive_collection_has_case_insensitive()
787+
{
788+
var input = """Tags ^$* "winner" """;
789+
var filterExpression = FilterParser.ParseFilter<Recipe>(input);
790+
filterExpression.ToString().Should()
791+
.Be(""""x => x.Tags.Any(z => (z.ToLower() == "winner".ToLower()))"""");
792+
}
793+
794+
[Fact]
795+
public void primitive_collection_does_not_have_case_insensitive()
796+
{
797+
var input = """Tags !^$* "winner" """;
798+
var filterExpression = FilterParser.ParseFilter<Recipe>(input);
799+
filterExpression.ToString().Should()
800+
.Be(""""x => x.Tags.Any(z => (z.ToLower() != "winner".ToLower()))"""");
801+
}
766802
}
767803

QueryKit.WebApiTestProject/Database/RecipeConfiguration.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ public sealed class RecipeConfiguration : IEntityTypeConfiguration<Recipe>
1111
/// </summary>
1212
public void Configure(EntityTypeBuilder<Recipe> builder)
1313
{
14+
builder.Property(x => x.Tags).HasColumnType("text[]");
15+
1416
// example for a simple 1:1 value object
1517
// builder.Property(x => x.Percent)
1618
// .HasConversion(x => x.Value, x => new Percent(x))
1719
// .HasColumnName("percent");
18-
20+
1921
// example for a more complex value object
2022
// builder.OwnsOne(x => x.PhysicalAddress, opts =>
2123
// {

QueryKit.WebApiTestProject/Entities/Recipes/Recipe.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ private set
3030
public DateOnly? DateOfOrigin { get; private set; }
3131

3232
public bool HaveMadeItMyself { get; private set; }
33+
34+
public List<string> Tags { get; set; } = new();
3335

3436
[JsonIgnore, IgnoreDataMember]
3537
public Author Author { get; private set; }
@@ -78,6 +80,12 @@ public Recipe SetIngredients(List<Ingredient> ingredients)
7880
_ingredients = ingredients;
7981
return this;
8082
}
83+
84+
public Recipe SetTags(List<string> tags)
85+
{
86+
Tags = tags;
87+
return this;
88+
}
8189

8290
protected Recipe() { } // For EF + Mocking
8391
}

QueryKit.WebApiTestProject/Migrations/20230722192729_BaseTestingMigration.Designer.cs renamed to QueryKit.WebApiTestProject/Migrations/20230724020352_BaseTestingMigration.Designer.cs

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

QueryKit.WebApiTestProject/Migrations/20230722192729_BaseTestingMigration.cs renamed to QueryKit.WebApiTestProject/Migrations/20230724020352_BaseTestingMigration.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using Microsoft.EntityFrameworkCore.Migrations;
34

45
#nullable disable
@@ -51,7 +52,8 @@ protected override void Up(MigrationBuilder migrationBuilder)
5152
directions = table.Column<string>(type: "text", nullable: false),
5253
rating = table.Column<int>(type: "integer", nullable: true),
5354
date_of_origin = table.Column<DateOnly>(type: "date", nullable: true),
54-
have_made_it_myself = table.Column<bool>(type: "boolean", nullable: false)
55+
have_made_it_myself = table.Column<bool>(type: "boolean", nullable: false),
56+
tags = table.Column<List<string>>(type: "text[]", nullable: false)
5557
},
5658
constraints: table =>
5759
{

QueryKit.WebApiTestProject/Migrations/TestingDbContextModelSnapshot.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// <auto-generated />
22
using System;
3+
using System.Collections.Generic;
34
using Microsoft.EntityFrameworkCore;
45
using Microsoft.EntityFrameworkCore.Infrastructure;
56
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@@ -141,6 +142,11 @@ protected override void BuildModel(ModelBuilder modelBuilder)
141142
.HasColumnType("integer")
142143
.HasColumnName("rating");
143144

145+
b.Property<List<string>>("Tags")
146+
.IsRequired()
147+
.HasColumnType("text[]")
148+
.HasColumnName("tags");
149+
144150
b.Property<string>("Title")
145151
.IsRequired()
146152
.HasColumnType("text")

QueryKit/Configuration/QueryKitConfiguration.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public interface IQueryKitConfiguration
2929
public string HasCountLessThanOperator { get; set; }
3030
public string HasCountGreaterThanOrEqualOperator { get; set; }
3131
public string HasCountLessThanOrEqualOperator { get; set; }
32+
public string HasOperator { get; set; }
33+
public string DoesNotHaveOperator { get; set; }
3234
}
3335

3436
public class QueryKitConfiguration : IQueryKitConfiguration
@@ -55,6 +57,8 @@ public class QueryKitConfiguration : IQueryKitConfiguration
5557
public string HasCountLessThanOperator { get; set; }
5658
public string HasCountGreaterThanOrEqualOperator { get; set; }
5759
public string HasCountLessThanOrEqualOperator { get; set; }
60+
public string HasOperator { get; set; }
61+
public string DoesNotHaveOperator { get; set; }
5862
public string CaseInsensitiveAppendix { get; set; }
5963
public string AndOperator { get; set; }
6064
public string OrOperator { get; set; }
@@ -94,5 +98,7 @@ public QueryKitConfiguration(Action<QueryKitSettings> configureSettings)
9498
HasCountLessThanOperator = settings.HasCountLessThanOperator;
9599
HasCountGreaterThanOrEqualOperator = settings.HasCountGreaterThanOrEqualOperator;
96100
HasCountLessThanOrEqualOperator = settings.HasCountLessThanOrEqualOperator;
101+
HasOperator = settings.HasOperator;
102+
DoesNotHaveOperator = settings.DoesNotHaveOperator;
97103
}
98104
}

QueryKit/Configuration/QueryKitSettings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public class QueryKitSettings
2727
public string HasCountLessThanOperator { get; set; } = ComparisonOperator.HasCountLessThanOperator().Operator();
2828
public string HasCountGreaterThanOrEqualOperator { get; set; } = ComparisonOperator.HasCountGreaterThanOrEqualOperator().Operator();
2929
public string HasCountLessThanOrEqualOperator { get; set; } = ComparisonOperator.HasCountLessThanOrEqualOperator().Operator();
30+
public string HasOperator { get; set; } = ComparisonOperator.HasOperator().Operator();
31+
public string DoesNotHaveOperator { get; set; } = ComparisonOperator.DoesNotHaveOperator().Operator();
3032
public string AndOperator { get; set; } = LogicalOperator.AndOperator.Operator();
3133
public string OrOperator { get; set; } = LogicalOperator.OrOperator.Operator();
3234
public string CaseInsensitiveAppendix { get; set; } = ComparisonOperator.CaseSensitiveAppendix.ToString();

QueryKit/FilterParser.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ from rest in Parse.LetterOrDigit.XOr(Parse.Char('_')).Many()
6767
.Or(Parse.String(ComparisonOperator.HasCountLessThanOrEqualOperator().Operator()).Text())
6868
.Or(Parse.String(ComparisonOperator.HasCountGreaterThanOperator().Operator()).Text())
6969
.Or(Parse.String(ComparisonOperator.HasCountLessThanOperator().Operator()).Text())
70+
.Or(Parse.String(ComparisonOperator.HasOperator().Operator()).Text())
71+
.Or(Parse.String(ComparisonOperator.DoesNotHaveOperator().Operator()).Text())
7072
.SelectMany(op => Parse.Char(ComparisonOperator.CaseSensitiveAppendix).Optional(), (op, caseInsensitive) => new { op, caseInsensitive, hasHash })
7173
.Select(x => ComparisonOperator.GetByOperatorString(x.op, x.caseInsensitive.IsDefined, x.hasHash)));
7274

QueryKit/Operators/ComparisonOperator.cs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public abstract class ComparisonOperator : SmartEnum<ComparisonOperator>
3030
public static ComparisonOperator CaseSensitiveHasCountLessThanOperator = new HasCountLessThanType();
3131
public static ComparisonOperator CaseSensitiveHasCountGreaterThanOrEqualOperator = new HasCountGreaterThanOrEqualType();
3232
public static ComparisonOperator CaseSensitiveHasCountLessThanOrEqualOperator = new HasCountLessThanOrEqualType();
33+
public static ComparisonOperator CaseSensitiveHasOperator = new HasType();
34+
public static ComparisonOperator CaseSensitiveDoesNotHaveOperator = new DoesNotHaveType();
3335

3436
public static ComparisonOperator EqualsOperator(bool caseInsensitive = false, bool usesAll = false) => new EqualsType(caseInsensitive);
3537
public static ComparisonOperator NotEqualsOperator(bool caseInsensitive = false, bool usesAll = false) => new NotEqualsType(caseInsensitive);
@@ -52,6 +54,8 @@ public abstract class ComparisonOperator : SmartEnum<ComparisonOperator>
5254
public static ComparisonOperator HasCountLessThanOperator(bool caseInsensitive = false, bool usesAll = false) => new HasCountLessThanType(caseInsensitive);
5355
public static ComparisonOperator HasCountGreaterThanOrEqualOperator(bool caseInsensitive = false, bool usesAll = false) => new HasCountGreaterThanOrEqualType(caseInsensitive);
5456
public static ComparisonOperator HasCountLessThanOrEqualOperator(bool caseInsensitive = false, bool usesAll = false) => new HasCountLessThanOrEqualType(caseInsensitive);
57+
public static ComparisonOperator HasOperator(bool caseInsensitive = false, bool usesAll = false) => new HasType(caseInsensitive);
58+
public static ComparisonOperator DoesNotHaveOperator(bool caseInsensitive = false, bool usesAll = false) => new DoesNotHaveType(caseInsensitive);
5559

5660

5761
public static ComparisonOperator GetByOperatorString(string op, bool caseInsensitive = false, bool usesAll = false)
@@ -148,6 +152,14 @@ public static ComparisonOperator GetByOperatorString(string op, bool caseInsensi
148152
{
149153
newOperator = new HasCountLessThanOrEqualType(caseInsensitive, usesAll);
150154
}
155+
if (comparisonOperator is HasType)
156+
{
157+
newOperator = new HasType(caseInsensitive, usesAll);
158+
}
159+
if (comparisonOperator is DoesNotHaveType)
160+
{
161+
newOperator = new DoesNotHaveType(caseInsensitive, usesAll);
162+
}
151163

152164
return newOperator == null
153165
? throw new Exception($"Operator {op} is not supported")
@@ -628,6 +640,49 @@ public override Expression GetExpression<T>(Expression left, Expression right, T
628640
}
629641
}
630642

643+
private class HasType : ComparisonOperator
644+
{
645+
public HasType(bool caseInsensitive = false, bool usesAll = false) : base("^$", 21, caseInsensitive, usesAll)
646+
{
647+
}
648+
649+
public override string Operator() => CaseInsensitive ? $"{Name}{CaseSensitiveAppendix}" : Name;
650+
public override Expression GetExpression<T>(Expression left, Expression right, Type? dbContextType)
651+
{
652+
if (left.Type.IsGenericType &&
653+
(left.Type.GetGenericTypeDefinition() == typeof(List<>) ||
654+
left.Type.GetGenericTypeDefinition() == typeof(ICollection<>) ||
655+
left.Type.GetGenericTypeDefinition() == typeof(IList<>) ||
656+
typeof(IEnumerable<>).IsAssignableFrom(left.Type.GetGenericTypeDefinition())))
657+
{
658+
return GetCollectionExpression(left, right, Expression.Equal, UsesAll);
659+
}
660+
661+
throw new Exception("DoesNotHaveType is only supported for collections");
662+
}
663+
}
664+
665+
private class DoesNotHaveType : ComparisonOperator
666+
{
667+
public DoesNotHaveType(bool caseInsensitive = false, bool usesAll = false) : base("!^$", 22, caseInsensitive, usesAll)
668+
{
669+
}
670+
671+
public override string Operator() => CaseInsensitive ? $"{Name}{CaseSensitiveAppendix}" : Name;
672+
public override Expression GetExpression<T>(Expression left, Expression right, Type? dbContextType)
673+
{
674+
if (left.Type.IsGenericType &&
675+
(left.Type.GetGenericTypeDefinition() == typeof(List<>) ||
676+
left.Type.GetGenericTypeDefinition() == typeof(ICollection<>) ||
677+
left.Type.GetGenericTypeDefinition() == typeof(IList<>) ||
678+
typeof(IEnumerable<>).IsAssignableFrom(left.Type.GetGenericTypeDefinition())))
679+
{
680+
return GetCollectionExpression(left, right, Expression.NotEqual, UsesAll);
681+
}
682+
683+
throw new Exception("DoesNotHaveType is only supported for collections");
684+
}
685+
}
631686

632687
internal class ComparisonAliasMatch
633688
{
@@ -734,6 +789,16 @@ internal static List<ComparisonAliasMatch> GetAliasMatches(IQueryKitConfiguratio
734789
matches.Add(new ComparisonAliasMatch { Alias = aliases.HasCountLessThanOrEqualOperator, Operator = HasCountLessThanOrEqualOperator().Operator() });
735790
matches.Add(new ComparisonAliasMatch { Alias = $"{aliases.HasCountLessThanOrEqualOperator}{caseInsensitiveAppendix}", Operator = $"{HasCountLessThanOrEqualOperator(true).Operator()}" });
736791
}
792+
if(aliases.HasOperator != HasOperator().Operator())
793+
{
794+
matches.Add(new ComparisonAliasMatch { Alias = aliases.HasOperator, Operator = HasOperator().Operator() });
795+
matches.Add(new ComparisonAliasMatch { Alias = $"{aliases.HasOperator}{caseInsensitiveAppendix}", Operator = $"{HasOperator(true).Operator()}" });
796+
}
797+
if(aliases.DoesNotHaveOperator != DoesNotHaveOperator().Operator())
798+
{
799+
matches.Add(new ComparisonAliasMatch { Alias = aliases.DoesNotHaveOperator, Operator = DoesNotHaveOperator().Operator() });
800+
matches.Add(new ComparisonAliasMatch { Alias = $"{aliases.DoesNotHaveOperator}{caseInsensitiveAppendix}", Operator = $"{DoesNotHaveOperator(true).Operator()}" });
801+
}
737802

738803
return matches;
739804
}
@@ -792,7 +857,7 @@ private Expression GetCollectionExpression(Expression left, Expression right, st
792857
: Expression.Call(anyMethod, left, anyLambda);
793858
}
794859

795-
public Expression GetCountExpression(Expression left, Expression right, string methodName)
860+
private Expression GetCountExpression(Expression left, Expression right, string methodName)
796861
{
797862
var leftAsEnumerableType = left.Type.GetInterface(nameof(IEnumerable));
798863
if (leftAsEnumerableType == null)

0 commit comments

Comments
 (0)