Skip to content
Open
891 changes: 891 additions & 0 deletions docs/specs/SPEC-filtering-has-syntax.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,17 @@
{ typeof(OnlyExpression), typeof(OnlyExpressionEvaluator) },
{ typeof(PropertyComparisonExpression), typeof(PropertyComparisonExpressionEvaluator) },
{ typeof(TextSearchExpression), typeof(TextSearchExpressionEvaluator) },
{ typeof(ActionComparisonExpression), typeof(ActionComparisonExpressionEvaluator) }
{ typeof(ActionComparisonExpression), typeof(ActionComparisonExpressionEvaluator) },
{ typeof(HasExpression), typeof(HasExpressionEvaluator) }
};
}

public IExpressionEvaluator GetEvaluator(FilterExpression expression)
{
if (expression == null)
{
throw new ArgumentNullException(nameof(expression));
}

Check warning on line 40 in src/ByteSync.Client/Business/Filtering/Evaluators/ExpressionEvaluatorFactory.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'ArgumentNullException.ThrowIfNull' instead of explicitly throwing a new exception instance

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbJ2VwhrarSxhaNM&open=AZrPMbJ2VwhrarSxhaNM&pullRequest=243

var expressionType = expression.GetType();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using ByteSync.Business.Filtering.Expressions;
using ByteSync.Models.Comparisons.Result;

namespace ByteSync.Business.Filtering.Evaluators;

public class HasExpressionEvaluator : ExpressionEvaluator<HasExpression>
{
public override bool Evaluate(HasExpression expression, ComparisonItem item)
{
return expression.ExpressionType switch
{
HasExpressionType.AccessIssue => EvaluateAccessIssue(item),
HasExpressionType.ComputationError => EvaluateComputationError(item),
HasExpressionType.SyncError => EvaluateSyncError(item),
_ => throw new ArgumentException($"Unknown HasExpressionType: {expression.ExpressionType}")
};
}

private bool EvaluateAccessIssue(ComparisonItem item)

Check warning on line 19 in src/ByteSync.Client/Business/Filtering/Evaluators/HasExpressionEvaluator.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'EvaluateAccessIssue' does not access instance data and can be marked as static

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbJhVwhrarSxhaNJ&open=AZrPMbJhVwhrarSxhaNJ&pullRequest=243

Check warning on line 19 in src/ByteSync.Client/Business/Filtering/Evaluators/HasExpressionEvaluator.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make 'EvaluateAccessIssue' a static method.

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbJhVwhrarSxhaNG&open=AZrPMbJhVwhrarSxhaNG&pullRequest=243
{
return item.ContentIdentities.Any(ci => ci.HasAccessIssue);
}

private bool EvaluateComputationError(ComparisonItem item)

Check warning on line 24 in src/ByteSync.Client/Business/Filtering/Evaluators/HasExpressionEvaluator.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'EvaluateComputationError' does not access instance data and can be marked as static

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbJhVwhrarSxhaNK&open=AZrPMbJhVwhrarSxhaNK&pullRequest=243

Check warning on line 24 in src/ByteSync.Client/Business/Filtering/Evaluators/HasExpressionEvaluator.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make 'EvaluateComputationError' a static method.

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbJhVwhrarSxhaNH&open=AZrPMbJhVwhrarSxhaNH&pullRequest=243
{
return item.ContentIdentities.Any(ci => ci.HasAnalysisError);
}

private bool EvaluateSyncError(ComparisonItem item)

Check warning on line 29 in src/ByteSync.Client/Business/Filtering/Evaluators/HasExpressionEvaluator.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make 'EvaluateSyncError' a static method.

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbJhVwhrarSxhaNI&open=AZrPMbJhVwhrarSxhaNI&pullRequest=243

Check warning on line 29 in src/ByteSync.Client/Business/Filtering/Evaluators/HasExpressionEvaluator.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'EvaluateSyncError' does not access instance data and can be marked as static

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbJhVwhrarSxhaNL&open=AZrPMbJhVwhrarSxhaNL&pullRequest=243
{
return item.ItemSynchronizationStatus.IsErrorStatus;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace ByteSync.Business.Filtering.Expressions;

public class HasExpression : FilterExpression
{
public HasExpressionType ExpressionType { get; }

public HasExpression(HasExpressionType expressionType)
{
ExpressionType = expressionType;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ByteSync.Business.Filtering.Expressions;

public enum HasExpressionType
{
AccessIssue,
ComputationError,
SyncError
}

126 changes: 83 additions & 43 deletions src/ByteSync.Client/Business/Filtering/Parsing/FilterParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@
/// Determines if the filter text contains patterns that indicate a complex expression
/// rather than a simple text search
/// </summary>
private bool IsComplexExpression(string filterText)

Check warning on line 53 in src/ByteSync.Client/Business/Filtering/Parsing/FilterParser.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'IsComplexExpression' does not access instance data and can be marked as static

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbIkVwhrarSxhaNF&open=AZrPMbIkVwhrarSxhaNF&pullRequest=243
{
// Split by whitespace to analyze individual terms
var terms = filterText.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

Check warning on line 56 in src/ByteSync.Client/Business/Filtering/Parsing/FilterParser.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbIkVwhrarSxhaND&open=AZrPMbIkVwhrarSxhaND&pullRequest=243

return terms.Any(term =>

Expand All @@ -77,6 +77,7 @@
term.Contains('=') ||

// Special operators/keywords
term.StartsWith(Identifiers.OPERATOR_HAS, StringComparison.OrdinalIgnoreCase) ||
term.StartsWith(Identifiers.OPERATOR_ACTIONS, StringComparison.OrdinalIgnoreCase) ||
term.StartsWith(Identifiers.OPERATOR_NAME, StringComparison.OrdinalIgnoreCase) ||
term.StartsWith(Identifiers.OPERATOR_PATH, StringComparison.OrdinalIgnoreCase) ||
Expand All @@ -91,9 +92,9 @@
/// <summary>
/// Creates a text search expression for simple text queries
/// </summary>
private ParseResult CreateTextSearchExpression(string filterText)

Check warning on line 95 in src/ByteSync.Client/Business/Filtering/Parsing/FilterParser.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'CreateTextSearchExpression' does not access instance data and can be marked as static

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbIkVwhrarSxhaNE&open=AZrPMbIkVwhrarSxhaNE&pullRequest=243
{
var terms = filterText.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

Check warning on line 97 in src/ByteSync.Client/Business/Filtering/Parsing/FilterParser.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbIkVwhrarSxhaNC&open=AZrPMbIkVwhrarSxhaNC&pullRequest=243

FilterExpression compositeExpression = new TrueExpression();
foreach (var term in terms)
Expand Down Expand Up @@ -352,59 +353,43 @@
}

if (CurrentToken?.Type == FilterTokenType.Identifier &&
CurrentToken.Token.Equals(Identifiers.OPERATOR_ACTIONS, StringComparison.OrdinalIgnoreCase))
CurrentToken.Token.Equals(Identifiers.OPERATOR_HAS, StringComparison.OrdinalIgnoreCase))
{
var actionPath = CurrentToken.Token.ToLowerInvariant();
NextToken();

while (CurrentToken?.Type == FilterTokenType.Dot)
if (CurrentToken?.Type != FilterTokenType.Colon)
{
NextToken();
if (CurrentToken?.Type != FilterTokenType.Identifier)
{
return ParseResult.Incomplete("Expected identifier after dot in action path");
}

actionPath += "." + CurrentToken?.Token.ToLowerInvariant();
NextToken();
return ParseResult.Incomplete($"Expected colon after '{Identifiers.OPERATOR_HAS}'");
}

if (CurrentToken?.Type == FilterTokenType.End || CurrentToken?.Type == FilterTokenType.LogicalOperator)
NextToken();
if (CurrentToken?.Type != FilterTokenType.Identifier && CurrentToken?.Type != FilterTokenType.String)
{
return ParseResult.Incomplete($"Expected identifier after '{Identifiers.OPERATOR_HAS}:'");
}

var hasType = CurrentToken.Token.ToLowerInvariant();
NextToken();

if (hasType == Identifiers.PROPERTY_ACCESS_ISSUE)
{
return ParseResult.Success(new HasExpression(HasExpressionType.AccessIssue));
}
else if (hasType == Identifiers.PROPERTY_COMPUTATION_ERROR)
{
return ParseResult.Success(new HasExpression(HasExpressionType.ComputationError));
}
else if (hasType == Identifiers.PROPERTY_SYNC_ERROR)
{
return ParseResult.Success(new HasExpression(HasExpressionType.SyncError));
}
else if (hasType == Identifiers.OPERATOR_ACTIONS)
{
return ParseResult.Success(new ActionComparisonExpression(actionPath, ComparisonOperator.GreaterThan, 0));
return ParseActionsExpression();
}
else
{
if (CurrentToken?.Type != FilterTokenType.Operator)
{
return ParseResult.Incomplete("Expected operator after action path");
}

var op = CurrentToken.Token;
NextToken();

try
{
var comparisonOperator = _operatorParser.Parse(op);

if (CurrentToken?.Type != FilterTokenType.Number && CurrentToken?.Type != FilterTokenType.DateTime)
{
return ParseResult.Incomplete("Expected numeric value / dateTime after operator in action comparison");
}

if (!int.TryParse(CurrentToken?.Token, out var value))
{
return ParseResult.Incomplete("Invalid numeric value in action comparison");
}

NextToken();

return ParseResult.Success(new ActionComparisonExpression(actionPath, comparisonOperator, value));
}
catch (ArgumentException ex)
{
return ParseResult.Incomplete(ex.Message);
}
return ParseResult.Incomplete($"Unknown has type: {hasType}");
}
}

Expand Down Expand Up @@ -534,6 +519,61 @@
return ParseResult.Success(textSearchExpression);
}

private ParseResult ParseActionsExpression()
{
var actionPath = Identifiers.OPERATOR_ACTIONS;

while (CurrentToken?.Type == FilterTokenType.Dot)
{
NextToken();
if (CurrentToken?.Type != FilterTokenType.Identifier)
{
return ParseResult.Incomplete("Expected identifier after dot in action path");
}

actionPath += "." + CurrentToken.Token.ToLowerInvariant();

Check warning on line 534 in src/ByteSync.Client/Business/Filtering/Parsing/FilterParser.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a StringBuilder instead.

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbIkVwhrarSxhaNB&open=AZrPMbIkVwhrarSxhaNB&pullRequest=243
NextToken();
}

if (CurrentToken?.Type == FilterTokenType.End ||
CurrentToken?.Type == FilterTokenType.LogicalOperator ||
CurrentToken?.Type == FilterTokenType.CloseParenthesis)
{
return ParseResult.Success(new ActionComparisonExpression(actionPath, ComparisonOperator.GreaterThan, 0));
}

if (CurrentToken?.Type != FilterTokenType.Operator)
{
return ParseResult.Incomplete("Expected operator after action path");
}

var op = CurrentToken.Token;
NextToken();

try
{
var comparisonOperator = _operatorParser.Parse(op);

if (CurrentToken?.Type != FilterTokenType.Number)
{
return ParseResult.Incomplete("Expected numeric value after operator in action comparison");
}

if (!int.TryParse(CurrentToken.Token, out var value))
{
return ParseResult.Incomplete("Invalid numeric value in action comparison");
}

NextToken();

return ParseResult.Success(new ActionComparisonExpression(actionPath, comparisonOperator, value));
}
catch (ArgumentException ex)
{
return ParseResult.Incomplete(ex.Message);
}
}

private void NextToken()
{
CurrentToken = _tokenizer.GetNextToken();
Expand Down
5 changes: 5 additions & 0 deletions src/ByteSync.Client/Business/Filtering/Parsing/Identifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@
public const string OPERATOR_ON = "on";
public const string OPERATOR_ONLY = "only";
public const string OPERATOR_IS = "is";
public const string OPERATOR_HAS = "has";
public const string OPERATOR_ACTIONS = "actions";
public const string OPERATOR_NAME = "name";
public const string OPERATOR_PATH = "path";

public const string PROPERTY_ACCESS_ISSUE = "access-issue";
public const string PROPERTY_COMPUTATION_ERROR = "computation-error";
public const string PROPERTY_SYNC_ERROR = "sync-error";

public const string PROPERTY_CONTENTS = "contents";
public const string PROPERTY_CONTENTS_AND_DATE = "contents-and-date";
public const string PROPERTY_LAST_WRITE_TIME = "last-write-time";
Expand All @@ -35,10 +40,10 @@
{
if (_cachedAll == null)
{
_cachedAll = typeof(Identifiers)
.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.FlattenHierarchy)
.Where(field => field.IsLiteral && !field.IsInitOnly && field.FieldType == typeof(string))
.Select(field => (string)field.GetValue(null))

Check warning on line 46 in src/ByteSync.Client/Business/Filtering/Parsing/Identifiers.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Converting null literal or possible null value to non-nullable type.

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbGQVwhrarSxhaM_&open=AZrPMbGQVwhrarSxhaM_&pullRequest=243
.ToList();

Check warning on line 47 in src/ByteSync.Client/Business/Filtering/Parsing/Identifiers.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Nullability of reference types in value of type 'List<string?>' doesn't match target type 'List<string>'.

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbGQVwhrarSxhaNA&open=AZrPMbGQVwhrarSxhaNA&pullRequest=243
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
RegisterType<PropertyComparisonExpressionEvaluator, IExpressionEvaluator>();
RegisterType<TextSearchExpressionEvaluator, IExpressionEvaluator>();
RegisterType<ActionComparisonExpressionEvaluator, IExpressionEvaluator>();
RegisterType<HasExpressionEvaluator, IExpressionEvaluator>();

RegisterType<ExpressionEvaluatorFactory, IExpressionEvaluatorFactory>();
RegisterType<FilterParser>();
Expand All @@ -60,14 +61,14 @@
_logger = Container.Resolve<ILogger<FilterService>>();
}

protected ComparisonItem CreateBasicComparisonItem(FileSystemTypes fileSystemType = FileSystemTypes.File,

Check warning on line 64 in tests/ByteSync.Client.IntegrationTests/Business/Filtering/BaseTestFiltering.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'CreateBasicComparisonItem' does not access instance data and can be marked as static

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbK-VwhrarSxhaNP&open=AZrPMbK-VwhrarSxhaNP&pullRequest=243
string filePath = "/file1.txt", string fileName = "file1.txt")
{
var pathIdentity = new PathIdentity(fileSystemType, filePath, fileName, filePath);
return new ComparisonItem(pathIdentity);
}

protected (FileDescription, InventoryPart) CreateFileDescription(

Check warning on line 71 in tests/ByteSync.Client.IntegrationTests/Business/Filtering/BaseTestFiltering.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'CreateFileDescription' does not access instance data and can be marked as static

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbK-VwhrarSxhaNQ&open=AZrPMbK-VwhrarSxhaNQ&pullRequest=243
string inventoryId,
string rootPath,
DateTime lastWriteTime,
Expand Down Expand Up @@ -96,7 +97,7 @@
return (fileDescription, inventoryPart);
}

protected (DirectoryDescription, InventoryPart) CreateDirectoryDescription(

Check warning on line 100 in tests/ByteSync.Client.IntegrationTests/Business/Filtering/BaseTestFiltering.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'CreateDirectoryDescription' does not access instance data and can be marked as static

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZrPMbK-VwhrarSxhaNR&open=AZrPMbK-VwhrarSxhaNR&pullRequest=243
string inventoryId,
string rootPath)
{
Expand Down Expand Up @@ -301,4 +302,97 @@
var evaluator = _evaluatorFactory.GetEvaluator(expression);
return evaluator.Evaluate(expression, item);
}

protected ComparisonItem PrepareComparisonWithInaccessibleFile(string dataPartId)
{
var comparisonItem = CreateBasicComparisonItem(FileSystemTypes.File);

string letter = dataPartId[0].ToString();

var inventory = new Inventory { InventoryId = $"Id_{letter}", Code = letter };
var inventoryPart = new InventoryPart(inventory, $"/testRoot{letter}", FileSystemTypes.Directory);
inventoryPart.Code = $"{letter}1";

var fileDescription = new FileDescription
{
InventoryPart = inventoryPart,
RelativePath = "/inaccessible_file.txt",
IsAccessible = false
};

var contentIdentity = new ContentIdentity(null);
contentIdentity.Add(fileDescription);
comparisonItem.AddContentIdentity(contentIdentity);

var dataParts = new Dictionary<string, (InventoryPart, FileDescription)>
{
{ dataPartId, (inventoryPart, fileDescription) }
};
ConfigureDataPartIndex(dataParts);

return comparisonItem;
}

protected ComparisonItem PrepareComparisonWithInaccessibleDirectory(string dataPartId)
{
var comparisonItem = CreateBasicComparisonItem(FileSystemTypes.Directory);

string letter = dataPartId[0].ToString();

var inventory = new Inventory { InventoryId = $"Id_{letter}", Code = letter };
var inventoryPart = new InventoryPart(inventory, $"/testRoot{letter}", FileSystemTypes.Directory);
inventoryPart.Code = $"{letter}1";

var directoryDescription = new DirectoryDescription
{
InventoryPart = inventoryPart,
RelativePath = "/inaccessible_dir",
IsAccessible = false
};

var contentIdentity = new ContentIdentity(null);
contentIdentity.Add(directoryDescription);
contentIdentity.AddAccessIssue(inventoryPart);
comparisonItem.AddContentIdentity(contentIdentity);

var dataParts = new Dictionary<string, (InventoryPart, DirectoryDescription)>
{
{ dataPartId, (inventoryPart, directoryDescription) }
};
ConfigureDataPartIndex(dataParts);

return comparisonItem;
}

protected ComparisonItem PrepareComparisonWithAnalysisError(string dataPartId)
{
var comparisonItem = CreateBasicComparisonItem(FileSystemTypes.File);

string letter = dataPartId[0].ToString();

var inventory = new Inventory { InventoryId = $"Id_{letter}", Code = letter };
var inventoryPart = new InventoryPart(inventory, $"/testRoot{letter}", FileSystemTypes.Directory);
inventoryPart.Code = $"{letter}1";

var fileDescription = new FileDescription
{
InventoryPart = inventoryPart,
RelativePath = "/error_file.txt",
IsAccessible = true,
AnalysisErrorDescription = "Simulated analysis error",
AnalysisErrorType = "TestError"
};

var contentIdentity = new ContentIdentity(null);
contentIdentity.Add(fileDescription);
comparisonItem.AddContentIdentity(contentIdentity);

var dataParts = new Dictionary<string, (InventoryPart, FileDescription)>
{
{ dataPartId, (inventoryPart, fileDescription) }
};
ConfigureDataPartIndex(dataParts);

return comparisonItem;
}
}
Loading
Loading