diff --git a/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs index 416303b06b..47af9f8b74 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs @@ -52,9 +52,7 @@ public LogicalExpression(LogicalOperator @operator, IImmutableList terms = filters.WhereNotNull().ToImmutableArray(); + ImmutableArray terms = [.. filters.WhereNotNull()]; return terms.Length > 1 ? new LogicalExpression(@operator, terms) : terms.FirstOrDefault(); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/NullConstantExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/NullConstantExpression.cs index 9685b6625c..9a0e9c7ddf 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/NullConstantExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/NullConstantExpression.cs @@ -35,17 +35,7 @@ public override string ToFullString() public override bool Equals(object? obj) { - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj is null || GetType() != obj.GetType()) - { - return false; - } - - return true; + return ReferenceEquals(this, obj); } public override int GetHashCode() diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Queries/QueryExpressionTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Queries/QueryExpressionTests.cs new file mode 100644 index 0000000000..ad2d4a8d89 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/UnitTests/Queries/QueryExpressionTests.cs @@ -0,0 +1,367 @@ +using System.Collections.Immutable; +using FluentAssertions; +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace JsonApiDotNetCoreTests.UnitTests.Queries; + +public sealed class QueryExpressionTests +{ + public static IEnumerable ExpressionTestData => + new QueryExpression[][] + { + [ + TestExpressionFactory.Instance.Any(), + TestExpressionFactory.Instance.Any() + ], + [ + TestExpressionFactory.Instance.Comparison(), + TestExpressionFactory.Instance.Comparison() + ], + [ + TestExpressionFactory.Instance.Count(), + TestExpressionFactory.Instance.Count() + ], + [ + TestExpressionFactory.Instance.Has(), + TestExpressionFactory.Instance.Has() + ], + [ + TestExpressionFactory.Instance.IncludeElement(), + TestExpressionFactory.Instance.IncludeElement() + ], + [ + TestExpressionFactory.Instance.Include(), + TestExpressionFactory.Instance.Include() + ], + [ + TestExpressionFactory.Instance.IsType(), + TestExpressionFactory.Instance.IsType() + ], + [ + TestExpressionFactory.Instance.LiteralConstant(), + TestExpressionFactory.Instance.LiteralConstant() + ], + [ + TestExpressionFactory.Instance.Logical(), + TestExpressionFactory.Instance.Logical() + ], + [ + TestExpressionFactory.Instance.MatchText(), + TestExpressionFactory.Instance.MatchText() + ], + [ + TestExpressionFactory.Instance.Not(), + TestExpressionFactory.Instance.Not() + ], + [ + TestExpressionFactory.Instance.NullConstant(), + TestExpressionFactory.Instance.NullConstant() + ], + [ + TestExpressionFactory.Instance.PaginationElementQueryStringValue(), + TestExpressionFactory.Instance.PaginationElementQueryStringValue() + ], + [ + TestExpressionFactory.Instance.Pagination(), + TestExpressionFactory.Instance.Pagination() + ], + [ + TestExpressionFactory.Instance.PaginationQueryStringValue(), + TestExpressionFactory.Instance.PaginationQueryStringValue() + ], + [ + TestExpressionFactory.Instance.QueryableHandler(), + TestExpressionFactory.Instance.QueryableHandler() + ], + [ + TestExpressionFactory.Instance.QueryStringParameterScope(), + TestExpressionFactory.Instance.QueryStringParameterScope() + ], + [ + TestExpressionFactory.Instance.ResourceFieldChainForText(), + TestExpressionFactory.Instance.ResourceFieldChainForText() + ], + [ + TestExpressionFactory.Instance.ResourceFieldChainForParent(), + TestExpressionFactory.Instance.ResourceFieldChainForParent() + ], + [ + TestExpressionFactory.Instance.ResourceFieldChainForChildren(), + TestExpressionFactory.Instance.ResourceFieldChainForChildren() + ], + [ + TestExpressionFactory.Instance.SortElement(), + TestExpressionFactory.Instance.SortElement() + ], + [ + TestExpressionFactory.Instance.Sort(), + TestExpressionFactory.Instance.Sort() + ], + [ + TestExpressionFactory.Instance.SparseFieldSet(), + TestExpressionFactory.Instance.SparseFieldSet() + ], + [ + TestExpressionFactory.Instance.SparseFieldTable(), + TestExpressionFactory.Instance.SparseFieldTable() + ] + }; + + [Theory] + [MemberData(nameof(ExpressionTestData))] + public void Expressions_are_equal(QueryExpression left, QueryExpression right) + { + // Assert + left.Equals(right).Should().BeTrue(); + right.Equals(left).Should().BeTrue(); + + // ReSharper disable once EqualExpressionComparison + left.Equals(left).Should().BeTrue(); + } + + [Theory] + [MemberData(nameof(ExpressionTestData))] + public void Expressions_are_not_equal_to_null(QueryExpression left, QueryExpression right) + { + // Assert + left.Equals(null).Should().BeFalse(); + right.Equals(null).Should().BeFalse(); + } + + [Theory] + [MemberData(nameof(ExpressionTestData))] + public void Expressions_have_same_hash_code(QueryExpression left, QueryExpression right) + { + // Assert + left.GetHashCode().Should().Be(right.GetHashCode()); + } + + [Theory] + [MemberData(nameof(ExpressionTestData))] + public void Expressions_convert_to_same_string(QueryExpression left, QueryExpression right) + { + // Assert + left.ToString().Should().Be(right.ToString()); + } + + [Theory] + [MemberData(nameof(ExpressionTestData))] + public void Expressions_convert_to_same_full_string(QueryExpression left, QueryExpression right) + { + // Assert + left.ToFullString().Should().Be(right.ToFullString()); + } + + [Theory] + [MemberData(nameof(ExpressionTestData))] + public void Expressions_have_same_return_type(QueryExpression left, QueryExpression right) + { + if (left is FunctionExpression leftFunction && right is FunctionExpression rightFunction) + { + // Assert + leftFunction.ReturnType.Should().Be(rightFunction.ReturnType); + } + } + + [Theory] + [MemberData(nameof(ExpressionTestData))] + public void Expressions_can_accept_visitor(QueryExpression left, QueryExpression right) + { + // Assert + left.Accept(EmptyQueryExpressionVisitor.Instance, null).Should().BeNull(); + right.Accept(EmptyQueryExpressionVisitor.Instance, null).Should().BeNull(); + } + + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + private class BaseTestResource : Identifiable + { + [Attr] + public string? Text { get; set; } + + [HasOne] + public BaseTestResource? Parent { get; set; } + + [HasMany] + public ISet Children { get; set; } = new HashSet(); + } + + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + private sealed class DerivedTestResource : BaseTestResource; + + private sealed class TestExpressionFactory + { + private readonly ResourceType _baseTestResourceType; + private readonly ResourceType _derivedTestResourceType; + private readonly AttrAttribute _textAttribute; + private readonly RelationshipAttribute _parentRelationship; + private readonly RelationshipAttribute _childrenRelationship; + public static TestExpressionFactory Instance { get; } = new(); + + private TestExpressionFactory() + { + var options = new JsonApiOptions(); + + var builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance); + builder.Add(); + builder.Add(); + IResourceGraph resourceGraph = builder.Build(); + + _baseTestResourceType = resourceGraph.GetResourceType(); + _derivedTestResourceType = resourceGraph.GetResourceType(); + _textAttribute = _baseTestResourceType.GetAttributeByPropertyName(nameof(BaseTestResource.Text)); + _parentRelationship = _baseTestResourceType.GetRelationshipByPropertyName(nameof(BaseTestResource.Parent)); + _childrenRelationship = _baseTestResourceType.GetRelationshipByPropertyName(nameof(BaseTestResource.Children)); + } + + public AnyExpression Any() + { + return new AnyExpression(ResourceFieldChainForText(), [LiteralConstant()]); + } + + public ComparisonExpression Comparison() + { + return new ComparisonExpression(ComparisonOperator.Equals, ResourceFieldChainForText(), LiteralConstant()); + } + + public CountExpression Count() + { + return new CountExpression(ResourceFieldChainForChildren()); + } + + public HasExpression Has() + { + return new HasExpression(ResourceFieldChainForChildren(), Comparison()); + } + + public IncludeElementExpression IncludeElement() + { + return new IncludeElementExpression(_parentRelationship, [new IncludeElementExpression(_childrenRelationship)]); + } + + public IncludeExpression Include() + { + return new IncludeExpression([IncludeElement()]); + } + + public IsTypeExpression IsType() + { + return new IsTypeExpression(ResourceFieldChainForParent(), _derivedTestResourceType, Has()); + } + + public LiteralConstantExpression LiteralConstant() + { + return new LiteralConstantExpression("example"); + } + + public LogicalExpression Logical() + { + return new LogicalExpression(LogicalOperator.Or, Comparison(), MatchText()); + } + + public MatchTextExpression MatchText() + { + return new MatchTextExpression(ResourceFieldChainForText(), LiteralConstant(), TextMatchKind.Contains); + } + + public NotExpression Not() + { + return new NotExpression(Comparison()); + } + + public NullConstantExpression NullConstant() + { + return NullConstantExpression.Instance; + } + + public PaginationElementQueryStringValueExpression PaginationElementQueryStringValue() + { + return new PaginationElementQueryStringValueExpression(ResourceFieldChainForChildren(), 5, 8); + } + + public PaginationExpression Pagination() + { + return new PaginationExpression(new PageNumber(2), new PageSize(5)); + } + + public PaginationQueryStringValueExpression PaginationQueryStringValue() + { + return new PaginationQueryStringValueExpression([PaginationElementQueryStringValue()]); + } + + public QueryableHandlerExpression QueryableHandler() + { +#pragma warning disable CS8974 // Converting method group to non-delegate type + object handler = TestQueryableHandler; +#pragma warning restore CS8974 // Converting method group to non-delegate type + return new QueryableHandlerExpression(handler, "disableCache"); + } + + public QueryStringParameterScopeExpression QueryStringParameterScope() + { + return new QueryStringParameterScopeExpression(LiteralConstant(), ResourceFieldChainForChildren()); + } + + public ResourceFieldChainExpression ResourceFieldChainForText() + { + return new ResourceFieldChainExpression(_textAttribute); + } + + public ResourceFieldChainExpression ResourceFieldChainForParent() + { + return new ResourceFieldChainExpression([_parentRelationship]); + } + + public ResourceFieldChainExpression ResourceFieldChainForChildren() + { + return new ResourceFieldChainExpression([_childrenRelationship]); + } + + public SortElementExpression SortElement() + { + return new SortElementExpression(Count(), false); + } + + public SortExpression Sort() + { + return new SortExpression([SortElement()]); + } + + public SparseFieldSetExpression SparseFieldSet() + { + return new SparseFieldSetExpression([ + _textAttribute, + _childrenRelationship + ]); + } + + public SparseFieldTableExpression SparseFieldTable() + { + return new SparseFieldTableExpression(new Dictionary + { + [_baseTestResourceType] = SparseFieldSet(), + [_derivedTestResourceType] = SparseFieldSet() + }.ToImmutableDictionary()); + } + + private static IQueryable TestQueryableHandler(IQueryable source, StringValues parameterValue) + { + throw new NotImplementedException(); + } + } + + private sealed class EmptyQueryExpressionVisitor : QueryExpressionVisitor + { + public static EmptyQueryExpressionVisitor Instance { get; } = new(); + + private EmptyQueryExpressionVisitor() + { + } + } +}