mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add has and enums support
This commit is contained in:
parent
07afbdaa4b
commit
13ddeaaf0a
@ -8,6 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" Version="7.1.0" />
|
||||
<PackageReference Include="Dapper" Version="2.1.24" />
|
||||
<PackageReference Include="EntityFrameworkCore.Projectables" Version="4.1.4-prebeta" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2023.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
|
||||
|
@ -28,6 +28,25 @@ using Sprache;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Utils;
|
||||
|
||||
public static class ParseHelper
|
||||
{
|
||||
public static Parser<T> ErrorMessage<T>(this Parser<T> @this, string message) =>
|
||||
input =>
|
||||
{
|
||||
IResult<T> result = @this(input);
|
||||
|
||||
return result.WasSuccessful
|
||||
? result
|
||||
: Result.Failure<T>(result.Remainder, message, result.Expectations);
|
||||
};
|
||||
|
||||
public static Parser<T> Error<T>(string message) =>
|
||||
input =>
|
||||
{
|
||||
return Result.Failure<T>(input, message, Array.Empty<string>());
|
||||
};
|
||||
}
|
||||
|
||||
public abstract record Filter
|
||||
{
|
||||
public static Filter<T>? And<T>(params Filter<T>?[] filters)
|
||||
@ -45,29 +64,29 @@ public abstract record Filter
|
||||
|
||||
public abstract record Filter<T> : Filter
|
||||
{
|
||||
public record And(Filter<T> first, Filter<T> second) : Filter<T>;
|
||||
public record And(Filter<T> First, Filter<T> Second) : Filter<T>;
|
||||
|
||||
public record Or(Filter<T> first, Filter<T> second) : Filter<T>;
|
||||
public record Or(Filter<T> First, Filter<T> Second) : Filter<T>;
|
||||
|
||||
public record Not(Filter<T> filter) : Filter<T>;
|
||||
public record Not(Filter<T> Filter) : Filter<T>;
|
||||
|
||||
public record Eq(string property, object value) : Filter<T>;
|
||||
public record Eq(string Property, object Value) : Filter<T>;
|
||||
|
||||
public record Ne(string property, object value) : Filter<T>;
|
||||
public record Ne(string Property, object Value) : Filter<T>;
|
||||
|
||||
public record Gt(string property, object value) : Filter<T>;
|
||||
public record Gt(string Property, object Value) : Filter<T>;
|
||||
|
||||
public record Ge(string property, object value) : Filter<T>;
|
||||
public record Ge(string Property, object Value) : Filter<T>;
|
||||
|
||||
public record Lt(string property, object value) : Filter<T>;
|
||||
public record Lt(string Property, object Value) : Filter<T>;
|
||||
|
||||
public record Le(string property, object value) : Filter<T>;
|
||||
public record Le(string Property, object Value) : Filter<T>;
|
||||
|
||||
public record Has(string property, object value) : Filter<T>;
|
||||
public record Has(string Property, object Value) : Filter<T>;
|
||||
|
||||
public record In(string property, object[] value) : Filter<T>;
|
||||
public record In(string Property, object[] Value) : Filter<T>;
|
||||
|
||||
public record Lambda(Expression<Func<T, bool>> lambda) : Filter<T>;
|
||||
public record Lambda(Expression<Func<T, bool>> Inner) : Filter<T>;
|
||||
|
||||
public static class FilterParsers
|
||||
{
|
||||
@ -79,7 +98,8 @@ public abstract record Filter<T> : Filter
|
||||
.Or(Parse.Ref(() => Gt))
|
||||
.Or(Parse.Ref(() => Ge))
|
||||
.Or(Parse.Ref(() => Lt))
|
||||
.Or(Parse.Ref(() => Le));
|
||||
.Or(Parse.Ref(() => Le))
|
||||
.Or(Parse.Ref(() => Has));
|
||||
|
||||
public static readonly Parser<Filter<T>> CompleteFilter =
|
||||
Parse.Ref(() => Or)
|
||||
@ -100,18 +120,70 @@ public abstract record Filter<T> : Filter
|
||||
.Or(Parse.String("||"))
|
||||
.Token();
|
||||
|
||||
public static readonly Parser<Filter<T>> And = Parse.ChainOperator(AndOperator, Filter, (_, a, b) => new Filter<T>.And(a, b));
|
||||
public static readonly Parser<Filter<T>> And = Parse.ChainOperator(AndOperator, Filter, (_, a, b) => new And(a, b));
|
||||
|
||||
public static readonly Parser<Filter<T>> Or = Parse.ChainOperator(OrOperator, And.Or(Filter), (_, a, b) => new Filter<T>.Or(a, b));
|
||||
public static readonly Parser<Filter<T>> Or = Parse.ChainOperator(OrOperator, And.Or(Filter), (_, a, b) => new Or(a, b));
|
||||
|
||||
public static readonly Parser<Filter<T>> Not =
|
||||
from not in Parse.IgnoreCase("not")
|
||||
.Or(Parse.String("!"))
|
||||
.Token()
|
||||
from filter in CompleteFilter
|
||||
select new Filter<T>.Not(filter);
|
||||
select new Not(filter);
|
||||
|
||||
private static Parser<Filter<T>> _GetOperationParser(Parser<object> op, Func<string, object, Filter<T>> apply)
|
||||
private static Parser<object> _GetValueParser(Type type)
|
||||
{
|
||||
if (type == typeof(int))
|
||||
return Parse.Number.Select(x => int.Parse(x) as object);
|
||||
if (type == typeof(float))
|
||||
{
|
||||
return
|
||||
from a in Parse.Number
|
||||
from dot in Parse.Char('.')
|
||||
from b in Parse.Number
|
||||
select float.Parse($"{a}.{b}") as object;
|
||||
}
|
||||
|
||||
if (type == typeof(string))
|
||||
{
|
||||
return (
|
||||
from lq in Parse.Char('"').Or(Parse.Char('\''))
|
||||
from str in Parse.AnyChar.Where(x => x is not '"' and not '\'').Many().Text()
|
||||
from rq in Parse.Char('"').Or(Parse.Char('\''))
|
||||
select str
|
||||
).Or(Parse.LetterOrDigit.Many().Text());
|
||||
}
|
||||
|
||||
if (type.IsEnum)
|
||||
{
|
||||
return Parse.LetterOrDigit.Many().Text().Then(x =>
|
||||
{
|
||||
if (Enum.TryParse(type, x, true, out object? value))
|
||||
return Parse.Return(value);
|
||||
return ParseHelper.Error<object>($"Invalid enum value. Unexpected {x}");
|
||||
});
|
||||
}
|
||||
|
||||
if (type == typeof(DateTime))
|
||||
{
|
||||
return
|
||||
from year in Parse.Digit.Repeat(4).Text().Select(int.Parse)
|
||||
from yd in Parse.Char('-')
|
||||
from mouth in Parse.Digit.Repeat(2).Text().Select(int.Parse)
|
||||
from md in Parse.Char('-')
|
||||
from day in Parse.Digit.Repeat(2).Text().Select(int.Parse)
|
||||
select new DateTime(year, mouth, day) as object;
|
||||
}
|
||||
|
||||
if (typeof(IEnumerable).IsAssignableFrom(type))
|
||||
return ParseHelper.Error<object>("Can't filter a list with a default comparator, use the 'has' filter.");
|
||||
return ParseHelper.Error<object>("Unfilterable field found");
|
||||
}
|
||||
|
||||
private static Parser<Filter<T>> _GetOperationParser(
|
||||
Parser<object> op,
|
||||
Func<string, object, Filter<T>> apply,
|
||||
Func<Type, Parser<object>>? customTypeParser = null)
|
||||
{
|
||||
Parser<string> property = Parse.LetterOrDigit.AtLeastOnce().Text();
|
||||
|
||||
@ -122,52 +194,11 @@ public abstract record Filter<T> : Filter
|
||||
.Select(x => x.GetProperty(prop, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance))
|
||||
.FirstOrDefault();
|
||||
if (propInfo == null)
|
||||
throw new ValidationException($"The given filter '{prop}' is invalid.");
|
||||
return ParseHelper.Error<Filter<T>>($"The given filter '{prop}' is invalid.");
|
||||
|
||||
Parser<object> value;
|
||||
|
||||
if (propInfo.PropertyType == typeof(int))
|
||||
value = Parse.Number.Select(x => int.Parse(x) as object);
|
||||
else if (propInfo.PropertyType == typeof(float))
|
||||
{
|
||||
value =
|
||||
from a in Parse.Number
|
||||
from dot in Parse.Char('.')
|
||||
from b in Parse.Number
|
||||
select float.Parse($"{a}.{b}") as object;
|
||||
}
|
||||
else if (propInfo.PropertyType == typeof(string))
|
||||
{
|
||||
value = (
|
||||
from lq in Parse.Char('"').Or(Parse.Char('\''))
|
||||
from str in Parse.AnyChar.Where(x => x is not '"' and not '\'').Many().Text()
|
||||
from rq in Parse.Char('"').Or(Parse.Char('\''))
|
||||
select str
|
||||
).Or(Parse.LetterOrDigit.Many().Text());
|
||||
}
|
||||
else if (propInfo.PropertyType.IsEnum)
|
||||
{
|
||||
value = Parse.LetterOrDigit.Many().Text().Select(x =>
|
||||
{
|
||||
if (Enum.TryParse(propInfo.PropertyType, x, true, out object? value))
|
||||
return value!;
|
||||
throw new ValidationException($"Invalid enum value. Unexpected {x}");
|
||||
});
|
||||
}
|
||||
else if (propInfo.PropertyType == typeof(DateTime))
|
||||
{
|
||||
value =
|
||||
from year in Parse.Digit.Repeat(4).Text().Select(int.Parse)
|
||||
from yd in Parse.Char('-')
|
||||
from mouth in Parse.Digit.Repeat(2).Text().Select(int.Parse)
|
||||
from md in Parse.Char('-')
|
||||
from day in Parse.Digit.Repeat(2).Text().Select(int.Parse)
|
||||
select new DateTime(year, mouth, day) as object;
|
||||
}
|
||||
else if (typeof(IEnumerable).IsAssignableFrom(propInfo.PropertyType))
|
||||
throw new ValidationException("Can't filter a list with a default comparator, use the 'in' filter.");
|
||||
else
|
||||
throw new ValidationException("Unfilterable field found");
|
||||
Parser<object> value = customTypeParser != null
|
||||
? customTypeParser(propInfo.PropertyType)
|
||||
: _GetValueParser(propInfo.PropertyType);
|
||||
|
||||
return
|
||||
from eq in op
|
||||
@ -205,6 +236,22 @@ public abstract record Filter<T> : Filter
|
||||
Parse.IgnoreCase("le").Or(Parse.String("<=")).Token(),
|
||||
(property, value) => new Le(property, value)
|
||||
);
|
||||
|
||||
public static readonly Parser<Filter<T>> Has = _GetOperationParser(
|
||||
Parse.IgnoreCase("has").Token(),
|
||||
(property, value) => new Has(property, value),
|
||||
(Type type) =>
|
||||
{
|
||||
if (typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string))
|
||||
return _GetValueParser(type.GetElementType() ?? type.GenericTypeArguments.First());
|
||||
return ParseHelper.Error<object>("Can't use 'has' on a non-list.");
|
||||
}
|
||||
);
|
||||
|
||||
// public static readonly Parser<Filter<T>> In = _GetOperationParser(
|
||||
// Parse.IgnoreCase("in").Token(),
|
||||
// (property, value) => new In(property, value)
|
||||
// );
|
||||
}
|
||||
|
||||
public static Filter<T>? From(string? filter)
|
||||
@ -212,9 +259,16 @@ public abstract record Filter<T> : Filter
|
||||
if (filter == null)
|
||||
return null;
|
||||
|
||||
IResult<Filter<T>> ret = FilterParsers.CompleteFilter.End().TryParse(filter);
|
||||
if (ret.WasSuccessful)
|
||||
return ret.Value;
|
||||
throw new ValidationException($"Could not parse filter argument: {ret.Message}. Not parsed: {filter[ret.Remainder.Position..]}");
|
||||
try
|
||||
{
|
||||
IResult<Filter<T>> ret = FilterParsers.CompleteFilter.End().TryParse(filter);
|
||||
if (ret.WasSuccessful)
|
||||
return ret.Value;
|
||||
throw new ValidationException($"Could not parse filter argument: {ret.Message}. Not parsed: {filter[ret.Remainder.Position..]}");
|
||||
}
|
||||
catch (ParseException ex)
|
||||
{
|
||||
throw new ValidationException($"Could not parse filter argument: {ex.Message}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
47
back/src/Kyoo.Abstractions/Utility/Wrapper.cs
Normal file
47
back/src/Kyoo.Abstractions/Utility/Wrapper.cs
Normal file
@ -0,0 +1,47 @@
|
||||
// Kyoo - A portable and vast media library solution.
|
||||
// Copyright (c) Kyoo.
|
||||
//
|
||||
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
//
|
||||
// Kyoo is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kyoo is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
|
||||
namespace Kyoo.Utils;
|
||||
|
||||
// Only used due to https://github.com/DapperLib/Dapper/issues/332
|
||||
public class Wrapper
|
||||
{
|
||||
public object Value { get; set; }
|
||||
|
||||
public Wrapper(object value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public class Handler : SqlMapper.TypeHandler<Wrapper>
|
||||
{
|
||||
public override Wrapper? Parse(object value)
|
||||
{
|
||||
throw new NotImplementedException("Wrapper should only be used to write");
|
||||
}
|
||||
|
||||
public override void SetValue(IDbDataParameter parameter, Wrapper? value)
|
||||
{
|
||||
parameter.Value = value?.Value;
|
||||
}
|
||||
}
|
||||
}
|
@ -202,6 +202,13 @@ namespace Kyoo.Core.Controllers
|
||||
return $"({ret})";
|
||||
}
|
||||
|
||||
object P(object value)
|
||||
{
|
||||
if (value is Enum)
|
||||
return new Wrapper(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
FormattableString Process(Filter<T> fil)
|
||||
{
|
||||
return fil switch
|
||||
@ -209,12 +216,13 @@ namespace Kyoo.Core.Controllers
|
||||
Filter<T>.And(var first, var second) => $"({Process(first)} and {Process(second)})",
|
||||
Filter<T>.Or(var first, var second) => $"({Process(first)} or {Process(second)})",
|
||||
Filter<T>.Not(var inner) => $"(not {Process(inner)})",
|
||||
Filter<T>.Eq(var property, var value) => Format(property, $"= {value}"),
|
||||
Filter<T>.Ne(var property, var value) => Format(property, $"!= {value}"),
|
||||
Filter<T>.Gt(var property, var value) => Format(property, $"> {value}"),
|
||||
Filter<T>.Ge(var property, var value) => Format(property, $">= {value}"),
|
||||
Filter<T>.Lt(var property, var value) => Format(property, $"< {value}"),
|
||||
Filter<T>.Le(var property, var value) => Format(property, $"> {value}"),
|
||||
Filter<T>.Eq(var property, var value) => Format(property, $"= {P(value)}"),
|
||||
Filter<T>.Ne(var property, var value) => Format(property, $"!= {P(value)}"),
|
||||
Filter<T>.Gt(var property, var value) => Format(property, $"> {P(value)}"),
|
||||
Filter<T>.Ge(var property, var value) => Format(property, $">= {P(value)}"),
|
||||
Filter<T>.Lt(var property, var value) => Format(property, $"< {P(value)}"),
|
||||
Filter<T>.Le(var property, var value) => Format(property, $"> {P(value)}"),
|
||||
Filter<T>.Has(var property, var value) => $"{P(value)} = any({_Property(property, config):raw})",
|
||||
Filter<T>.Lambda(var lambda) => throw new NotSupportedException(),
|
||||
};
|
||||
}
|
||||
|
@ -28,7 +28,6 @@ using Kyoo.Abstractions.Models;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
using Kyoo.Abstractions.Models.Exceptions;
|
||||
using Kyoo.Abstractions.Models.Utils;
|
||||
using Kyoo.Core.Api;
|
||||
using Kyoo.Postgresql;
|
||||
using Kyoo.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -133,6 +132,7 @@ namespace Kyoo.Core.Controllers
|
||||
Filter<T>.Ge(var property, var value) => Expression.GreaterThanOrEqual(Expression.Property(x, property), Expression.Constant(value)),
|
||||
Filter<T>.Lt(var property, var value) => Expression.LessThan(Expression.Property(x, property), Expression.Constant(value)),
|
||||
Filter<T>.Le(var property, var value) => Expression.LessThanOrEqual(Expression.Property(x, property), Expression.Constant(value)),
|
||||
Filter<T>.Has(var property, var value) => Expression.Call(typeof(Enumerable), "Contains", new[] { value.GetType() }, Expression.Property(x, property), Expression.Constant(value)),
|
||||
Filter<T>.Lambda(var lambda) => ExpressionArgumentReplacer.ReplaceParams(lambda.Body, lambda.Parameters, x),
|
||||
};
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCore.Proxy" Version="4.4.0" />
|
||||
<PackageReference Include="Blurhash.SkiaSharp" Version="2.0.0" />
|
||||
<PackageReference Include="Dapper" Version="2.1.21" />
|
||||
<PackageReference Include="Dapper" Version="2.1.24" />
|
||||
<PackageReference Include="InterpolatedSql.Dapper" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.12" />
|
||||
|
@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.21" />
|
||||
<PackageReference Include="Dapper" Version="2.1.24" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="7.0.2" />
|
||||
<PackageReference Include="EntityFrameworkCore.Projectables" Version="4.1.4-prebeta" />
|
||||
<PackageReference Include="InterpolatedSql.Dapper" Version="2.1.0" />
|
||||
|
@ -25,6 +25,7 @@ using InterpolatedSql.SqlBuilders;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models;
|
||||
using Kyoo.Postgresql.Utils;
|
||||
using Kyoo.Utils;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@ -88,6 +89,7 @@ namespace Kyoo.Postgresql
|
||||
SqlMapper.AddTypeHandler(typeof(Dictionary<string, MetadataId>), new JsonTypeHandler<Dictionary<string, MetadataId>>());
|
||||
SqlMapper.AddTypeHandler(typeof(List<string>), new ListTypeHandler<string>());
|
||||
SqlMapper.AddTypeHandler(typeof(List<Genre>), new ListTypeHandler<Genre>());
|
||||
SqlMapper.AddTypeHandler(typeof(Wrapper), new Wrapper.Handler());
|
||||
InterpolatedSqlBuilderOptions.DefaultOptions.ReuseIdenticalParameters = true;
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user