0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-06-29 18:21:08 +00:00
CleanArchitecture/CleanArchitecture.Application/ViewModels/Sorting/SortQuery.cs
2023-09-09 21:21:23 +02:00

300 lines
9.5 KiB
C#

using System;
using System.Collections.ObjectModel;
using Microsoft.AspNetCore.Mvc;
namespace CleanArchitecture.Application.ViewModels.Sorting;
public sealed class SortQuery
{
private string? _query = string.Empty;
public ReadOnlyCollection<SortParameter> Parameters { get; private set; } = new(Array.Empty<SortParameter>());
[FromQuery(Name = "order_by")]
public string? Query
{
get => _query;
set
{
_query = value;
Parameters = ParseQuery(_query);
}
}
public static ReadOnlyCollection<SortParameter> ParseQuery(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return new ReadOnlyCollection<SortParameter>(Array.Empty<SortParameter>());
}
if (value.Length > short.MaxValue)
{
throw new ArgumentException($"sort query can not be longer than {short.MaxValue} characters");
}
value = value.ToLower();
if (!value.Contains(','))
{
return new ReadOnlyCollection<SortParameter>(new[] { GetParam(value) });
}
var @params = value.Split(',');
var parsedParams = new SortParameter[@params.Length];
for (var i = 0; i < @params.Length; i++)
{
parsedParams[i] = GetParam(@params[i]);
}
return new ReadOnlyCollection<SortParameter>(parsedParams);
}
private static SortParameter GetParam(string value)
{
value = value.Trim();
var queryInfo = FindTokens(value);
if (queryInfo.OpeningBracketIndex > 0)
{
// asc(name), desc(name), ascending(name), descending(name)
return GetSortParamFromFunctionalStyle(value, queryInfo);
}
// i.e. "name asc", "name descending" or similar
if (queryInfo.FirstSpaceIndex >= 0)
{
return GetSortParamFromSentence(value, queryInfo);
}
// name, +name, -name, name+, name-
return GetSortParamFromSingleWord(value, queryInfo);
}
private static SortParameter GetSortParamFromSentence(string value, QueryInfo info)
{
var secondWordStartIndex = FindNextNonWhitespaceCharacter(value, info.FirstSpaceIndex + 1);
if (secondWordStartIndex < 0)
{
throw new ArgumentException("Expected query string in form of \"{param} asc/desc\"");
}
var paramName = value[..info.FirstSpaceIndex];
var orderName = value[secondWordStartIndex..];
if (orderName == "asc" || orderName == "ascending")
{
return new SortParameter(paramName, SortOrder.Ascending);
}
if (orderName == "desc" || orderName == "descending")
{
return new SortParameter(paramName, SortOrder.Descending);
}
throw new ArgumentException(
$"Unsupported sort order {orderName}. Valid are 'asc', 'ascending', 'desc' or 'descending'");
}
private static SortParameter GetSortParamFromSingleWord(string value, QueryInfo info)
{
if (info.PlusSignIndex < 0 && info.MinusSignIndex < 0)
{
return new SortParameter(value, SortOrder.Ascending);
}
var order = info.PlusSignIndex >= 0 ? SortOrder.Ascending : SortOrder.Descending;
var indicatorIndex = Math.Max(info.MinusSignIndex, info.PlusSignIndex);
if (indicatorIndex == 0)
{
return new SortParameter(value[1..], order);
}
return new SortParameter(value[..indicatorIndex], order);
}
private static SortParameter GetSortParamFromFunctionalStyle(string value, QueryInfo info)
{
var param = value
.Substring(info.OpeningBracketIndex + 1, info.ClosingBracketIndex - info.OpeningBracketIndex - 1)
.Trim();
if (string.IsNullOrWhiteSpace(param))
{
throw new FormatException("Parameter name could not be extracted");
}
if (value.StartsWith("asc(") || value.StartsWith("ascending("))
{
return new SortParameter(param, SortOrder.Ascending);
}
if (value.StartsWith("desc(") || value.StartsWith("descending("))
{
return new SortParameter(param, SortOrder.Descending);
}
throw new FormatException("Unparsable sort query");
}
private static QueryInfo FindTokens(string query)
{
short plusSignIndex = -1;
short minusSignIndex = -1;
short firstSpaceIndex = -1;
short openingBracketIndex = -1;
short closingBracketIndex = -1;
for (short i = 0; i < query.Length; i++)
{
switch (query[i])
{
case '(':
if (openingBracketIndex >= 0)
{
throw new FormatException("Only one bracket is allowed in functional style queries");
}
if (plusSignIndex >= 0 || minusSignIndex >= 0)
{
throw new FormatException(
"Order indicator (\"+\", \"-\") can not be used together with functional style (i.e.\"(name)\")");
}
if (firstSpaceIndex >= 0)
{
throw new FormatException($"Unexpected whitespace at position {firstSpaceIndex + 1}");
}
openingBracketIndex = i;
break;
case ')':
if (closingBracketIndex >= 0)
{
throw new FormatException("Only one closing bracket is allowed in functional style queries");
}
if (openingBracketIndex < 0)
{
throw new FormatException("Closing brackets can only be places after opening brackets");
}
closingBracketIndex = i;
break;
case '+':
if (plusSignIndex >= 0)
{
throw new FormatException("Only one positive order indicator \"+\" is allowed per query");
}
if (minusSignIndex >= 0)
{
throw new FormatException("Only one order indicator (\"+\", \"-\") is allowed per query");
}
if (firstSpaceIndex >= 0)
{
throw new FormatException($"Unexpected whitespace at position {firstSpaceIndex + 1}");
}
plusSignIndex = i;
break;
case '-':
if (minusSignIndex >= 0)
{
throw new FormatException("Only one negative order indicator \"-\" is allowed per query");
}
if (plusSignIndex >= 0)
{
throw new FormatException("Only one order indicator (\"+\", \"-\") is allowed per query");
}
if (firstSpaceIndex >= 0)
{
throw new FormatException($"Unexpected whitespace at position {firstSpaceIndex + 1}");
}
minusSignIndex = i;
break;
case ' ':
if (firstSpaceIndex == -1)
{
firstSpaceIndex = i;
}
if (minusSignIndex >= 0 || plusSignIndex >= 0)
{
throw new FormatException($"Unexpected whitespace at position {i + 1}");
}
break;
default:
// Check for stuff after query end like "asc(name)blabla
// "+" and "-" can be either at the start or at the end, we are only interested
// in the case where it's at the end.
if (plusSignIndex > 0 || minusSignIndex > 0 || closingBracketIndex >= 0)
{
var endOfQuery = Math.Max(Math.Max(plusSignIndex, minusSignIndex), closingBracketIndex);
throw new FormatException($"End of query expected at {endOfQuery}");
}
break;
}
}
return new QueryInfo(
plusSignIndex,
minusSignIndex,
firstSpaceIndex,
openingBracketIndex,
closingBracketIndex);
}
private static int FindNextNonWhitespaceCharacter(string value, int startIndex)
{
for (var i = startIndex; i < value.Length; i++)
{
if (!char.IsWhiteSpace(value[i]))
{
return i;
}
}
return -1;
}
private readonly struct QueryInfo
{
public readonly short PlusSignIndex;
public readonly short MinusSignIndex;
public readonly short FirstSpaceIndex;
public readonly short OpeningBracketIndex;
public readonly short ClosingBracketIndex;
public QueryInfo(
short plusSignIndex,
short minusSignIndex,
short firstSpaceIndex,
short openingBracketIndex,
short closingBracketIndex)
{
PlusSignIndex = plusSignIndex;
MinusSignIndex = minusSignIndex;
FirstSpaceIndex = firstSpaceIndex;
OpeningBracketIndex = openingBracketIndex;
ClosingBracketIndex = closingBracketIndex;
}
}
}