Skip to content

Commit

Permalink
Search expression visitor updates (#491)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnstairs authored May 23, 2019
1 parent 420c5b0 commit c09f9ef
Show file tree
Hide file tree
Showing 20 changed files with 447 additions and 108 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using Microsoft.Health.Fhir.Core.Features.Search.Expressions;
using Microsoft.Health.Fhir.Core.Models;
using Xunit;

namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Search.Expressions
{
public class ExpressionRewriterTests
{
[Fact]
public void GivenANoopRewriter_WhenVisiting_ReturnsTheSameExpressionInstances()
{
var expressionRewriter = new NoopRewriter();

void VerifyVisit(Expression expression)
{
Assert.Same(expression, expression.AcceptVisitor(expressionRewriter, null));
}

var simpleExpression1 = Expression.Equals(FieldName.Number, null, 1M);
var simpleExpression2 = Expression.Equals(FieldName.Number, null, 5M);
VerifyVisit(simpleExpression1);
VerifyVisit(Expression.SearchParameter(new SearchParameterInfo("my-param"), simpleExpression1));
VerifyVisit(Expression.Chained("Observation", "subject", "Patient", simpleExpression1));
VerifyVisit(Expression.CompartmentSearch("Patient", "x"));
VerifyVisit(Expression.Missing(FieldName.Quantity, null));
VerifyVisit(Expression.MissingSearchParameter(new SearchParameterInfo("my-param"), true));
VerifyVisit(Expression.Or(simpleExpression1, simpleExpression2));
VerifyVisit(Expression.StringEquals(FieldName.String, null, "Bob", true));
}

[Fact]
public void GivenARewriterThatTurnsValuesIntoRanges_WhenVisiting_ReplacesSubexpressions()
{
var expressionRewriter = new RangeRewriter();

void VerifyVisit(string expected, Expression inputExpression)
{
Assert.Equal(expected, inputExpression.AcceptVisitor(expressionRewriter, null).ToString());
}

var simpleExpression1 = Expression.Equals(FieldName.Number, null, 1M);
var simpleExpression2 = Expression.Equals(FieldName.Number, null, 5M);
string expectedAndString1 = "(And (FieldGreaterThanOrEqual Number 0) (FieldLessThanOrEqual Number 2))";
string expectedAndString2 = "(And (FieldGreaterThanOrEqual Number 4) (FieldLessThanOrEqual Number 6))";

VerifyVisit(expectedAndString1, simpleExpression1);

VerifyVisit($"(Param my-param {expectedAndString1})", Expression.SearchParameter(new SearchParameterInfo("my-param"), simpleExpression1));

VerifyVisit($"(Chain subject:Patient {expectedAndString1})", Expression.Chained("Observation", "subject", "Patient", simpleExpression1));
VerifyVisit($"(Or {expectedAndString1} {expectedAndString2})", Expression.Or(simpleExpression1, simpleExpression2));
}

public class NoopRewriter : ExpressionRewriter<object>
{
}

public class RangeRewriter : ExpressionRewriter<object>
{
public override Expression VisitBinary(BinaryExpression expression, object context)
{
// turn "Field = x" into "Field >= (x-1) and Field < (x+1)
if (expression.BinaryOperator == BinaryOperator.Equal && expression.Value is decimal decimalValue)
{
return Expression.And(
Expression.GreaterThanOrEqual(fieldName: expression.FieldName, expression.ComponentIndex, decimalValue - 1),
Expression.LessThanOrEqual(fieldName: expression.FieldName, expression.ComponentIndex, decimalValue + 1));
}

return expression;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;
using Microsoft.Health.Fhir.Core.Features.Search.Expressions;
using Microsoft.Health.Fhir.Core.Models;
using Xunit;

namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Search.Expressions
{
public class ExpressionToStringTests
{
[Fact]
public void GivenAnExpression_WhenCallingToString_ReturnsAnUnderstandableString()
{
VerifyExpression("(FieldEqual Quantity 1)", Expression.Equals(FieldName.Quantity, null, 1));
VerifyExpression("(FieldEqual QuantityCode 'a')", Expression.Equals(FieldName.QuantityCode, null, "a"));
VerifyExpression("(FieldEqual [0].QuantityCode 'a')", Expression.Equals(FieldName.QuantityCode, 0, "a"));

VerifyExpression("(StringEquals TokenText 'a')", Expression.StringEquals(FieldName.TokenText, null, "a", false));
VerifyExpression("(StringEqualsIgnoreCase TokenText 'a')", Expression.StringEquals(FieldName.TokenText, null, "a", true));
VerifyExpression("(StringEqualsIgnoreCase [0].TokenText 'a')", Expression.StringEquals(FieldName.TokenText, 0, "a", true));

VerifyExpression("(Param my-param (FieldEqual Quantity 'a'))", Expression.SearchParameter(new SearchParameterInfo("my-param"), Expression.Equals(FieldName.Quantity, null, "a")));

VerifyExpression("(MissingParam my-param)", Expression.MissingSearchParameter(new SearchParameterInfo("my-param"), true));
VerifyExpression("(NotMissingParam my-param)", Expression.MissingSearchParameter(new SearchParameterInfo("my-param"), false));

VerifyExpression("(And (FieldGreaterThan Quantity 1) (FieldLessThan Quantity 10))", Expression.And(Expression.GreaterThan(FieldName.Quantity, null, 1), Expression.LessThan(FieldName.Quantity, null, 10)));

VerifyExpression("(MissingField Quantity)", Expression.Missing(FieldName.Quantity, null));
VerifyExpression("(MissingField [0].Quantity)", Expression.Missing(FieldName.Quantity, 0));

VerifyExpression("(Compartment Patient 'x')", Expression.CompartmentSearch("Patient", "x"));

VerifyExpression("(Chain subject:Patient (FieldGreaterThan DateTimeEnd 2000-01-01T00:00:00.0000000))", Expression.Chained("Observation", "subject", "Patient", Expression.GreaterThan(FieldName.DateTimeEnd, null, new DateTime(2000, 1, 1))));
}

private static void VerifyExpression(string expected, Expression expression)
{
Assert.Equal(expected, expression.ToString());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@ namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Search
{
public static class SearchExpressionTestHelper
{
internal static void ValidateSearchParameterExpression(IExpression expression, string paramName, Action<Expression> valueValidator)
internal static void ValidateSearchParameterExpression(Expression expression, string paramName, Action<Expression> valueValidator)
{
SearchParameterExpression parameterExpression = Assert.IsType<SearchParameterExpression>(expression);
Assert.Equal(paramName, parameterExpression.Parameter.Name);
valueValidator(parameterExpression.Expression);
}

internal static void ValidateResourceTypeSearchParameterExpression(IExpression expression, string typeName)
internal static void ValidateResourceTypeSearchParameterExpression(Expression expression, string typeName)
{
ValidateSearchParameterExpression(expression, SearchParameterNames.ResourceType, e => ValidateBinaryExpression(e, FieldName.TokenCode, BinaryOperator.Equal, typeName));
}

public static void ValidateChainedExpression(
IExpression expression,
Expression expression,
ResourceType resourceType,
string key,
string targetResourceType,
Expand All @@ -44,7 +44,7 @@ public static void ValidateChainedExpression(
}

public static void ValidateChainedExpression(
IExpression expression,
Expression expression,
Type resourceType,
string key,
Type targetResourceType,
Expand All @@ -60,7 +60,7 @@ public static void ValidateChainedExpression(
}

public static void ValidateMultiaryExpression(
IExpression expression,
Expression expression,
MultiaryOperator multiaryOperator,
params Action<Expression>[] valueValidators)
{
Expand All @@ -73,13 +73,13 @@ public static void ValidateMultiaryExpression(
valueValidators);
}

public static void ValidateEqualsExpression(IExpression expression, FieldName expectedFieldName, object expectedValue)
public static void ValidateEqualsExpression(Expression expression, FieldName expectedFieldName, object expectedValue)
{
ValidateBinaryExpression(expression, expectedFieldName, BinaryOperator.Equal, expectedValue);
}

public static void ValidateBinaryExpression(
IExpression expression,
Expression expression,
FieldName expectedFieldName,
BinaryOperator expectedBinaryOperator,
object expectedValue)
Expand All @@ -92,7 +92,7 @@ public static void ValidateBinaryExpression(
}

public static void ValidateStringExpression(
IExpression expression,
Expression expression,
FieldName expectedFieldName,
StringOperator expectedStringOperator,
string expectedValue,
Expand All @@ -107,7 +107,7 @@ public static void ValidateStringExpression(
}

public static void ValidateDateTimeBinaryOperatorExpression(
IExpression expression,
Expression expression,
FieldName expectedFieldName,
BinaryOperator expectedExpression,
DateTimeOffset expectedValue)
Expand All @@ -120,7 +120,7 @@ public static void ValidateDateTimeBinaryOperatorExpression(
}

public static void ValidateMissingParamExpression(
IExpression expression,
Expression expression,
string expectedParamName,
bool expectedIsMissing)
{
Expand All @@ -131,7 +131,7 @@ public static void ValidateMissingParamExpression(
}

public static void ValidateMissingFieldExpression(
IExpression expression,
Expression expression,
FieldName expectedFieldName)
{
MissingFieldExpression mfExpression = Assert.IsType<MissingFieldExpression>(expression);
Expand All @@ -140,7 +140,7 @@ public static void ValidateMissingFieldExpression(
}

public static void ValidateCompartmentSearchExpression(
IExpression expression,
Expression expression,
string compartmentType,
string compartmentId)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;
using System.Globalization;
using EnsureThat;

namespace Microsoft.Health.Fhir.Core.Features.Search.Expressions
Expand Down Expand Up @@ -45,11 +47,31 @@ public BinaryExpression(BinaryOperator binaryOperator, FieldName fieldName, int?
/// </summary>
public object Value { get; }

protected internal override void AcceptVisitor(IExpressionVisitor visitor)
public override TOutput AcceptVisitor<TContext, TOutput>(IExpressionVisitor<TContext, TOutput> visitor, TContext context)
{
EnsureArg.IsNotNull(visitor, nameof(visitor));

visitor.Visit(this);
return visitor.VisitBinary(this, context);
}

public override string ToString()
{
string ValueToString()
{
switch (Value)
{
case string _:
return $"'{Value}'";
case DateTime dt:
return dt.ToString("O");
case IFormattable f:
return f.ToString(null, CultureInfo.InvariantCulture);
default:
return Value?.ToString();
}
}

return $"(Field{BinaryOperator} {(ComponentIndex == null ? null : $"[{ComponentIndex}].")}{FieldName} {ValueToString()})";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,16 @@ public ChainedExpression(
/// </summary>
public Expression Expression { get; }

protected internal override void AcceptVisitor(IExpressionVisitor visitor)
public override TOutput AcceptVisitor<TContext, TOutput>(IExpressionVisitor<TContext, TOutput> visitor, TContext context)
{
EnsureArg.IsNotNull(visitor, nameof(visitor));

visitor.Visit(this);
return visitor.VisitChained(this, context);
}

public override string ToString()
{
return $"(Chain {ParamName}:{TargetResourceType} {Expression})";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,15 @@ public CompartmentSearchExpression(string compartmentType, string compartmentId)
/// </summary>
public string CompartmentId { get; }

protected internal override void AcceptVisitor(IExpressionVisitor visitor)
public override TOutput AcceptVisitor<TContext, TOutput>(IExpressionVisitor<TContext, TOutput> visitor, TContext context)
{
EnsureArg.IsNotNull(visitor, nameof(visitor));
visitor.Visit(this);
return visitor.VisitCompartment(this, context);
}

public override string ToString()
{
return $"(Compartment {CompartmentType} '{CompartmentId}')";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Microsoft.Health.Fhir.Core.Features.Search.Expressions
/// <summary>
/// Represents an expression.
/// </summary>
public abstract class Expression : IExpression
public abstract class Expression
{
/// <summary>
/// Creates a <see cref="SearchParameterExpression"/> that represents a set of ANDed expressions over a search parameter.
Expand Down Expand Up @@ -233,11 +233,9 @@ public static CompartmentSearchExpression CompartmentSearch(string compartmentTy
return new CompartmentSearchExpression(compartmentType, compartmentId);
}

protected internal abstract void AcceptVisitor(IExpressionVisitor visitor);
public abstract TOutput AcceptVisitor<TContext, TOutput>(IExpressionVisitor<TContext, TOutput> visitor, TContext context);

public void AcceptVisitor(object visitor)
{
AcceptVisitor((IExpressionVisitor)visitor);
}
/// <inheritdoc />
public abstract override string ToString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using EnsureThat;

namespace Microsoft.Health.Fhir.Core.Features.Search.Expressions
{
public static class ExpressionExtensions
{
/// <summary>
/// Calls <see cref="Expression.AcceptVisitor{TContext,TOutput}"/> using
/// <see cref="IExpressionVisitorWithInitialContext{TContext,TOutput}.InitialContext"/>
/// as with the context argument
/// </summary>
/// <typeparam name="TContext">The type of the context parameter</typeparam>
/// <typeparam name="TOutput">The return type of the visitor</typeparam>
/// <param name="expression">The expression</param>
/// <param name="visitor">The visitor</param>
/// <returns>The output from the visit.</returns>
public static TOutput AcceptVisitor<TContext, TOutput>(this Expression expression, IExpressionVisitorWithInitialContext<TContext, TOutput> visitor)
{
EnsureArg.IsNotNull(expression, nameof(expression));
EnsureArg.IsNotNull(visitor, nameof(visitor));

return expression.AcceptVisitor(visitor, visitor.InitialContext);
}
}
}
Loading

0 comments on commit c09f9ef

Please sign in to comment.