1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Condition operators - Redesigned API to leverage generics

Condition operators - Updated default operators to use new API
This commit is contained in:
Robert 2020-10-19 19:47:16 +02:00
parent cf16b9c218
commit 4e69395ce8
28 changed files with 404 additions and 307 deletions

View File

@ -1,12 +1,7 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core.DefaultTypes
namespace Artemis.Core.DefaultTypes
{
internal class EqualsConditionOperator : ConditionOperator
internal class EqualsConditionOperator : ConditionOperator<object, object>
{
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(object)};
public override string Description => "Equals";
public override string Icon => "Equal";

View File

@ -1,18 +1,13 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core.DefaultTypes
namespace Artemis.Core.DefaultTypes
{
internal class GreaterThanConditionOperator : ConditionOperator
internal class GreaterThanConditionOperator : ConditionOperator<double, double>
{
public override IReadOnlyCollection<Type> CompatibleTypes => Constants.NumberTypes;
public override string Description => "Is greater than";
public override string Icon => "GreaterThan";
public override bool Evaluate(object a, object b)
public override bool Evaluate(double a, double b)
{
return Convert.ToSingle(a) > Convert.ToSingle(b);
return a > b;
}
}
}

View File

@ -1,18 +1,13 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core.DefaultTypes
namespace Artemis.Core.DefaultTypes
{
internal class GreaterThanOrEqualConditionOperator : ConditionOperator
internal class GreaterThanOrEqualConditionOperator : ConditionOperator<double, double>
{
public override IReadOnlyCollection<Type> CompatibleTypes => Constants.NumberTypes;
public override string Description => "Is greater than or equal to";
public override string Icon => "GreaterThanOrEqual";
public override bool Evaluate(object a, object b)
public override bool Evaluate(double a, double b)
{
return Convert.ToSingle(a) >= Convert.ToSingle(b);
return a >= b;
}
}
}

View File

@ -1,18 +1,13 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core.DefaultTypes
namespace Artemis.Core.DefaultTypes
{
internal class LessThanConditionOperator : ConditionOperator
internal class LessThanConditionOperator : ConditionOperator<double, double>
{
public override IReadOnlyCollection<Type> CompatibleTypes => Constants.NumberTypes;
public override string Description => "Is less than";
public override string Icon => "LessThan";
public override bool Evaluate(object a, object b)
public override bool Evaluate(double a, double b)
{
return Convert.ToSingle(a) < Convert.ToSingle(b);
return a < b;
}
}
}

View File

@ -1,18 +1,13 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core.DefaultTypes
namespace Artemis.Core.DefaultTypes
{
internal class LessThanOrEqualConditionOperator : ConditionOperator
internal class LessThanOrEqualConditionOperator : ConditionOperator<double, double>
{
public override IReadOnlyCollection<Type> CompatibleTypes => Constants.NumberTypes;
public override string Description => "Is less than or equal to";
public override string Icon => "LessThanOrEqual";
public override bool Evaluate(object a, object b)
public override bool Evaluate(double a, double b)
{
return Convert.ToSingle(a) <= Convert.ToSingle(b);
return a <= b;
}
}
}

View File

@ -1,12 +1,7 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core.DefaultTypes
namespace Artemis.Core.DefaultTypes
{
internal class NotEqualConditionOperator : ConditionOperator
internal class NotEqualConditionOperator : ConditionOperator<object, object>
{
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(object)};
public override string Description => "Does not equal";
public override string Icon => "NotEqualVariant";

View File

@ -1,21 +1,11 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core.DefaultTypes
namespace Artemis.Core.DefaultTypes
{
internal class NotNullConditionOperator : ConditionOperator
internal class NotNullConditionOperator : ConditionOperator<object>
{
public NotNullConditionOperator()
{
SupportsRightSide = false;
}
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(object)};
public override string Description => "Is not null";
public override string Icon => "CheckboxMarkedCircleOutline";
public override bool Evaluate(object a, object b)
public override bool Evaluate(object a)
{
return a != null;
}

View File

@ -1,21 +1,11 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core.DefaultTypes
namespace Artemis.Core.DefaultTypes
{
internal class NullConditionOperator : ConditionOperator
internal class NullConditionOperator : ConditionOperator<object>
{
public NullConditionOperator()
{
SupportsRightSide = false;
}
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(object)};
public override string Description => "Is null";
public override string Icon => "Null";
public override bool Evaluate(object a, object b)
public override bool Evaluate(object a)
{
return a == null;
}

View File

@ -0,0 +1,16 @@
using System;
namespace Artemis.Core.DefaultTypes
{
internal class NumberEqualsConditionOperator : ConditionOperator<double, double>
{
public override string Description => "Equals";
public override string Icon => "Equal";
public override bool Evaluate(double a, double b)
{
// Numbers can be tricky, an epsilon like this is close enough
return Math.Abs(a - b) < 0.000001;
}
}
}

View File

@ -0,0 +1,16 @@
using System;
namespace Artemis.Core.DefaultTypes
{
internal class NumberNotEqualConditionOperator : ConditionOperator<double, double>
{
public override string Description => "Does not equal";
public override string Icon => "NotEqualVariant";
public override bool Evaluate(double a, double b)
{
// Numbers can be tricky, an epsilon like this is close enough
return Math.Abs(a - b) > 0.000001;
}
}
}

View File

@ -1,21 +1,15 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core.DefaultTypes
{
internal class StringContainsConditionOperator : ConditionOperator
internal class StringContainsConditionOperator : ConditionOperator<string, string>
{
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(string)};
public override string Description => "Contains";
public override string Icon => "Contain";
public override bool Evaluate(object a, object b)
public override bool Evaluate(string a, string b)
{
string aString = (string) a;
string bString = (string) b;
return bString != null && aString != null && aString.Contains(bString, StringComparison.InvariantCultureIgnoreCase);
return a != null && b != null && a.Contains(b, StringComparison.InvariantCultureIgnoreCase);
}
}
}

View File

@ -1,21 +1,15 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core.DefaultTypes
{
internal class StringEndsWithConditionOperator : ConditionOperator
internal class StringEndsWithConditionOperator : ConditionOperator<string, string>
{
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(string)};
public override string Description => "Ends with";
public override string Icon => "ContainEnd";
public override bool Evaluate(object a, object b)
public override bool Evaluate(string a, string b)
{
string aString = (string) a;
string bString = (string) b;
return bString != null && aString != null && aString.EndsWith(bString, StringComparison.InvariantCultureIgnoreCase);
return a != null && b != null && a.EndsWith(b, StringComparison.InvariantCultureIgnoreCase);
}
}
}

View File

@ -1,21 +1,15 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core.DefaultTypes
{
internal class StringEqualsConditionOperator : ConditionOperator
internal class StringEqualsConditionOperator : ConditionOperator<string, string>
{
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(string)};
public override string Description => "Equals";
public override string Icon => "Equal";
public override bool Evaluate(object a, object b)
public override bool Evaluate(string a, string b)
{
string aString = (string) a;
string bString = (string) b;
return string.Equals(aString, bString, StringComparison.InvariantCultureIgnoreCase);
return string.Equals(a, b, StringComparison.InvariantCultureIgnoreCase);
}
}
}

View File

@ -1,21 +1,15 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core.DefaultTypes
{
internal class StringNotContainsConditionOperator : ConditionOperator
internal class StringNotContainsConditionOperator : ConditionOperator<string, string>
{
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(string)};
public override string Description => "Does not contain";
public override string Icon => "FormatStrikethrough";
public override bool Evaluate(object a, object b)
public override bool Evaluate(string a, string b)
{
string aString = (string) a;
string bString = (string) b;
return bString != null && aString != null && !aString.Contains(bString, StringComparison.InvariantCultureIgnoreCase);
return a != null && b != null && !a.Contains(b, StringComparison.InvariantCultureIgnoreCase);
}
}
}

View File

@ -1,21 +1,15 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core.DefaultTypes
{
internal class StringNotEqualConditionOperator : ConditionOperator
internal class StringNotEqualConditionOperator : ConditionOperator<string, string>
{
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(string)};
public override string Description => "Does not equal";
public override string Icon => "NotEqualVariant";
public override bool Evaluate(object a, object b)
public override bool Evaluate(string a, string b)
{
string aString = (string) a;
string bString = (string) b;
return !string.Equals(aString, bString, StringComparison.InvariantCultureIgnoreCase);
return !string.Equals(a, b, StringComparison.InvariantCultureIgnoreCase);
}
}
}

View File

@ -1,21 +1,15 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core.DefaultTypes
{
internal class StringStartsWithConditionOperator : ConditionOperator
internal class StringStartsWithConditionOperator : ConditionOperator<string, string>
{
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(string)};
public override string Description => "Starts with";
public override string Icon => "ContainStart";
public override bool Evaluate(object a, object b)
public override bool Evaluate(string a, string b)
{
string aString = (string) a;
string bString = (string) b;
return bString != null && aString != null && aString.StartsWith(bString, StringComparison.InvariantCultureIgnoreCase);
return a != null && b != null && a.StartsWith(b, StringComparison.InvariantCultureIgnoreCase);
}
}
}

View File

@ -21,6 +21,25 @@ namespace Artemis.Core
{typeof(short), new List<Type> {typeof(byte)}}
};
private static readonly Dictionary<Type, string> TypeKeywords = new Dictionary<Type, string>
{
{typeof(bool), "bool"},
{typeof(byte), "byte"},
{typeof(sbyte), "sbyte"},
{typeof(char), "char"},
{typeof(decimal), "decimal"},
{typeof(double), "double"},
{typeof(float), "float"},
{typeof(int), "int"},
{typeof(uint), "uint"},
{typeof(long), "long"},
{typeof(ulong), "ulong"},
{typeof(short), "short"},
{typeof(ushort), "ushort"},
{typeof(object), "object"},
{typeof(string), "string"}
};
public static bool IsGenericType(this Type type, Type genericType)
{
if (type == null)
@ -64,31 +83,10 @@ namespace Artemis.Core
|| value is decimal;
}
private static readonly Dictionary<Type, string> TypeKeywords = new Dictionary<Type, string>()
{
{typeof(bool), "bool"},
{typeof(byte), "byte"},
{typeof(sbyte), "sbyte"},
{typeof(char), "char"},
{typeof(decimal), "decimal"},
{typeof(double), "double"},
{typeof(float), "float"},
{typeof(int), "int"},
{typeof(uint), "uint"},
{typeof(long), "long"},
{typeof(ulong), "ulong"},
{typeof(short), "short"},
{typeof(ushort), "ushort"},
{typeof(object), "object"},
{typeof(string), "string"},
};
// From https://stackoverflow.com/a/2224421/5015269 but inverted and renamed to match similar framework methods
/// <summary>
/// Determines whether an instance of a specified type can be casted to a variable of the current type
/// </summary>
/// <param name="to"></param>
/// <param name="from"></param>
/// <returns></returns>
public static bool IsCastableFrom(this Type to, Type from)
{
@ -99,14 +97,32 @@ namespace Artemis.Core
if (PrimitiveTypeConversions.ContainsKey(to) && PrimitiveTypeConversions[to].Contains(from))
return true;
bool castable = from.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Any(
m => m.ReturnType == to &&
(m.Name == "op_Implicit" ||
m.Name == "op_Explicit")
);
.Any(m => m.ReturnType == to && (m.Name == "op_Implicit" || m.Name == "op_Explicit"));
return castable;
}
/// <summary>
/// Scores how well the two types can be casted from one to another, 5 being a perfect match and 0 being not castable
/// at all
/// </summary>
/// <returns></returns>
public static int ScoreCastability(this Type to, Type from)
{
if (to == from)
return 5;
if (to.TypeIsNumber() && from.TypeIsNumber())
return 4;
if (PrimitiveTypeConversions.ContainsKey(to) && PrimitiveTypeConversions[to].Contains(from))
return 3;
bool castable = from.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Any(m => m.ReturnType == to && (m.Name == "op_Implicit" || m.Name == "op_Explicit"));
if (castable)
return 2;
if (to.IsAssignableFrom(from))
return 1;
return 0;
}
/// <summary>
/// Returns the default value of the given type
/// </summary>
@ -149,7 +165,7 @@ namespace Artemis.Core
}
/// <summary>
/// Determines a display name for the given type
/// Determines a display name for the given type
/// </summary>
/// <param name="type">The type to determine the name for</param>
/// <param name="humanize">Whether or not to humanize the result, defaults to false</param>

View File

@ -0,0 +1,65 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Represents a condition operator that performs a boolean operation
/// <para>
/// To implement your own condition operator, inherit <see cref="ConditionOperator{TLeftSide, TRightSide}" /> or
/// <see cref="ConditionOperator{TLeftSide}" />
/// </para>
/// </summary>
public abstract class BaseConditionOperator
{
/// <summary>
/// Gets or sets the description of this logical operator
/// </summary>
public abstract string Description { get; }
/// <summary>
/// Gets or sets the icon of this logical operator
/// </summary>
public abstract string Icon { get; }
/// <summary>
/// Gets the plugin info this condition operator belongs to
/// <para>Note: Not set until after registering</para>
/// </summary>
public PluginInfo PluginInfo { get; internal set; }
/// <summary>
/// Gets the left side type of this condition operator
/// </summary>
public abstract Type LeftSideType { get; }
/// <summary>
/// Gets the right side type of this condition operator. May be null if the operator does not support a left side type
/// </summary>
public abstract Type? RightSideType { get; }
/// <summary>
/// Returns whether the given type is supported by the operator
/// </summary>
/// <param name="type">The type to check for, must be either the same or be castable to the target type</param>
/// <param name="side">Which side of the operator to check, left or right</param>
public abstract bool SupportsType(Type type, ConditionParameterSide side);
/// <summary>
/// Evaluates the condition with the input types being provided as objects
/// <para>
/// This leaves the caller responsible for the types matching <see cref="LeftSideType" /> and
/// <see cref="RightSideType" />
/// </para>
/// </summary>
/// <param name="leftSideValue">The left side value, type should match <see cref="LeftSideType" /></param>
/// <param name="rightSideValue">The right side value, type should match <see cref="RightSideType" /></param>
/// <returns>The result of the boolean condition's evaluation</returns>
internal abstract bool InternalEvaluate(object? leftSideValue, object? rightSideValue);
}
public enum ConditionParameterSide
{
Left,
Right
}
}

View File

@ -0,0 +1,95 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Represents a condition operator that performs a boolean operation using a left- and right-side
/// </summary>
public abstract class ConditionOperator<TLeftSide, TRightSide> : BaseConditionOperator
{
/// <summary>
/// Evaluates the operator on a and b
/// </summary>
/// <param name="a">The parameter on the left side of the expression</param>
/// <param name="b">The parameter on the right side of the expression</param>
public abstract bool Evaluate(TLeftSide a, TRightSide b);
/// <inheritdoc />
public override bool SupportsType(Type type, ConditionParameterSide side)
{
if (type == null)
return true;
if (side == ConditionParameterSide.Left)
return LeftSideType.IsCastableFrom(type);
return RightSideType.IsCastableFrom(type);
}
/// <inheritdoc />
internal override bool InternalEvaluate(object? leftSideValue, object? rightSideValue)
{
// TODO: Can we avoid boxing/unboxing?
TLeftSide leftSide;
if (leftSideValue != null)
leftSide = (TLeftSide) Convert.ChangeType(leftSideValue, typeof(TLeftSide));
else
leftSide = default;
TRightSide rightSide;
if (rightSideValue != null)
rightSide = (TRightSide) Convert.ChangeType(rightSideValue, typeof(TRightSide));
else
rightSide = default;
return Evaluate(leftSide, rightSide);
}
/// <inheritdoc />
public override Type LeftSideType => typeof(TLeftSide);
/// <inheritdoc />
public override Type RightSideType => typeof(TRightSide);
}
/// <summary>
/// Represents a condition operator that performs a boolean operation using only a left side
/// </summary>
public abstract class ConditionOperator<TLeftSide> : BaseConditionOperator
{
/// <summary>
/// Evaluates the operator on a and b
/// </summary>
/// <param name="a">The parameter on the left side of the expression</param>
public abstract bool Evaluate(TLeftSide a);
/// <inheritdoc />
public override bool SupportsType(Type type, ConditionParameterSide side)
{
if (type == null)
return true;
if (side == ConditionParameterSide.Left)
return LeftSideType.IsCastableFrom(type);
return false;
}
/// <inheritdoc />
internal override bool InternalEvaluate(object? leftSideValue, object? rightSideValue)
{
// TODO: Can we avoid boxing/unboxing?
TLeftSide leftSide;
if (leftSideValue != null)
leftSide = (TLeftSide) Convert.ChangeType(leftSideValue, typeof(TLeftSide));
else
leftSide = default;
return Evaluate(leftSide);
}
/// <inheritdoc />
public override Type LeftSideType => typeof(TLeftSide);
/// <summary>
/// Always <c>null</c>, not applicable to this type of condition operator
/// </summary>
public override Type? RightSideType => null;
}
}

View File

@ -1,55 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Artemis.Core
{
/// <summary>
/// A condition operator is used by the conditions system to perform a specific boolean check
/// </summary>
public abstract class ConditionOperator
{
/// <summary>
/// Gets the plugin info this condition operator belongs to
/// <para>Note: Not set until after registering</para>
/// </summary>
public PluginInfo PluginInfo { get; internal set; }
/// <summary>
/// Gets the types this operator supports
/// </summary>
public abstract IReadOnlyCollection<Type> CompatibleTypes { get; }
/// <summary>
/// Gets or sets the description of this logical operator
/// </summary>
public abstract string Description { get; }
/// <summary>
/// Gets or sets the icon of this logical operator
/// </summary>
public abstract string Icon { get; }
/// <summary>
/// Gets or sets whether this condition operator supports a right side, defaults to true
/// </summary>
public bool SupportsRightSide { get; protected set; } = true;
/// <summary>
/// Returns whether the given type is supported by the operator
/// </summary>
public bool SupportsType(Type type)
{
if (type == null)
return true;
return CompatibleTypes.Any(t => t.IsCastableFrom(type));
}
/// <summary>
/// Evaluates the operator on a and b
/// </summary>
/// <param name="a">The parameter on the left side of the expression</param>
/// <param name="b">The parameter on the right side of the expression</param>
public abstract bool Evaluate(object? a, object? b);
}
}

View File

@ -47,7 +47,7 @@ namespace Artemis.Core
/// <summary>
/// Gets the operator
/// </summary>
public ConditionOperator? Operator { get; private set; }
public BaseConditionOperator? Operator { get; private set; }
/// <summary>
/// Gets the data model condition list this predicate belongs to
@ -99,6 +99,8 @@ namespace Artemis.Core
{
if (path != null && !path.IsValid)
throw new ArtemisCoreException("Cannot update right side of predicate to an invalid path");
if (Operator != null && path != null && !Operator.SupportsType(path.GetPropertyType()!, ConditionParameterSide.Right))
throw new ArtemisCoreException($"Selected operator does not support right side of type {path.GetPropertyType()!.Name}");
RightPath?.Dispose();
RightPath = path != null ? new DataModelPath(path) : null;
@ -123,7 +125,7 @@ namespace Artemis.Core
/// Updates the operator of the predicate and re-compiles the expression
/// </summary>
/// <param name="conditionOperator"></param>
public void UpdateOperator(ConditionOperator? conditionOperator)
public void UpdateOperator(BaseConditionOperator? conditionOperator)
{
if (conditionOperator == null)
{
@ -139,12 +141,13 @@ namespace Artemis.Core
}
Type leftType = LeftPath.GetPropertyType()!;
if (!conditionOperator.SupportsType(leftType))
// Left side can't go empty so enforce a match
if (!conditionOperator.SupportsType(leftType, ConditionParameterSide.Left))
throw new ArtemisCoreException($"Cannot apply operator {conditionOperator.GetType().Name} to this predicate because " +
$"it does not support left side type {leftType.Name}");
if (conditionOperator.SupportsType(leftType))
Operator = conditionOperator;
Operator = conditionOperator;
ValidateRightSide();
}
/// <summary>
@ -206,7 +209,7 @@ namespace Artemis.Core
if (leftSideValue != null && leftSideValue.GetType().IsValueType && RightStaticValue == null)
return false;
return Operator.Evaluate(leftSideValue, RightStaticValue);
return Operator.InternalEvaluate(leftSideValue, RightStaticValue);
}
if (RightPath == null || !RightPath.IsValid)
@ -217,8 +220,8 @@ namespace Artemis.Core
{
// If the path targets a property inside the list, evaluate on the list path value instead of the right path value
if (RightPath.Target is ListPredicateWrapperDataModel)
return Operator.Evaluate(GetListPathValue(LeftPath, target), GetListPathValue(RightPath, target));
return Operator.Evaluate(GetListPathValue(LeftPath, target), RightPath.GetValue());
return Operator.InternalEvaluate(GetListPathValue(LeftPath, target), GetListPathValue(RightPath, target));
return Operator.InternalEvaluate(GetListPathValue(LeftPath, target), RightPath.GetValue());
}
return false;
@ -300,7 +303,7 @@ namespace Artemis.Core
// Operator
if (Entity.OperatorPluginGuid != null)
{
ConditionOperator? conditionOperator = ConditionOperatorStore.Get(Entity.OperatorPluginGuid.Value, Entity.OperatorType)?.ConditionOperator;
BaseConditionOperator? conditionOperator = ConditionOperatorStore.Get(Entity.OperatorPluginGuid.Value, Entity.OperatorType)?.ConditionOperator;
if (conditionOperator != null)
UpdateOperator(conditionOperator);
}
@ -361,28 +364,31 @@ namespace Artemis.Core
return;
Type leftType = LeftPath.GetPropertyType()!;
if (!Operator.SupportsType(leftType))
if (!Operator.SupportsType(leftType, ConditionParameterSide.Left))
Operator = null;
}
private void ValidateRightSide()
{
Type? leftType = LeftPath?.GetPropertyType();
if (Operator == null)
return;
if (PredicateType == ProfileRightSideType.Dynamic)
{
if (RightPath == null || !RightPath.IsValid)
return;
Type rightSideType = RightPath.GetPropertyType()!;
if (leftType != null && !leftType.IsCastableFrom(rightSideType))
if (!Operator.SupportsType(rightSideType, ConditionParameterSide.Right))
UpdateRightSideDynamic(null);
}
else
{
if (RightStaticValue != null && (leftType == null || leftType.IsCastableFrom(RightStaticValue.GetType())))
UpdateRightSideStatic(RightStaticValue);
else
UpdateRightSideStatic(null);
if (RightStaticValue == null)
return;
if (!Operator.SupportsType(RightStaticValue.GetType(), ConditionParameterSide.Right))
UpdateRightSideDynamic(null);
}
}
@ -391,24 +397,27 @@ namespace Artemis.Core
RightPath?.Dispose();
RightPath = null;
// If the left side is empty simply apply the value, any validation will wait
if (LeftPath == null || !LeftPath.IsValid)
// If the operator is null simply apply the value, any validation will wait
if (Operator == null)
{
RightStaticValue = staticValue;
return;
}
// If the left path is valid we can expect a type
Type leftSideType = LeftPath.GetPropertyType()!;
// If the operator does not support a right side, always set it to null
if (Operator.RightSideType == null)
{
RightStaticValue = null;
return;
}
// If not null ensure the types match and if not, convert it
if (staticValue != null && staticValue.GetType() == leftSideType)
if (staticValue != null && staticValue.GetType() == Operator.RightSideType)
RightStaticValue = staticValue;
else if (staticValue != null)
RightStaticValue = Convert.ChangeType(staticValue, leftSideType);
RightStaticValue = Convert.ChangeType(staticValue, Operator.RightSideType);
// If null create a default instance for value types or simply make it null for reference types
else if (leftSideType.IsValueType)
RightStaticValue = Activator.CreateInstance(leftSideType);
else if (Operator.RightSideType.IsValueType)
RightStaticValue = Activator.CreateInstance(Operator.RightSideType);
else
RightStaticValue = null;
}
@ -426,7 +435,7 @@ namespace Artemis.Core
private void ConditionOperatorStoreOnConditionOperatorAdded(object sender, ConditionOperatorStoreEvent e)
{
ConditionOperator conditionOperator = e.Registration.ConditionOperator;
BaseConditionOperator conditionOperator = e.Registration.ConditionOperator;
if (Entity.OperatorPluginGuid == conditionOperator.PluginInfo.Guid && Entity.OperatorType == conditionOperator.GetType().Name)
UpdateOperator(conditionOperator);
}

View File

@ -1,4 +1,5 @@
using System;
using System.IO;
using Artemis.Core.DataModelExpansions;
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions;
@ -43,7 +44,7 @@ namespace Artemis.Core
/// <summary>
/// Gets the operator
/// </summary>
public ConditionOperator? Operator { get; private set; }
public BaseConditionOperator? Operator { get; private set; }
/// <summary>
/// Gets the path of the left property
@ -87,6 +88,8 @@ namespace Artemis.Core
{
if (path != null && !path.IsValid)
throw new ArtemisCoreException("Cannot update right side of predicate to an invalid path");
if (Operator != null && path != null && !Operator.SupportsType(path.GetPropertyType()!, ConditionParameterSide.Right))
throw new ArtemisCoreException($"Selected operator does not support right side of type {path.GetPropertyType()!.Name}");
RightPath?.Dispose();
RightPath = path != null ? new DataModelPath(path) : null;
@ -104,24 +107,27 @@ namespace Artemis.Core
RightPath?.Dispose();
RightPath = null;
// If the left side is empty simply apply the value, any validation will wait
if (LeftPath == null || !LeftPath.IsValid)
// If the operator is null simply apply the value, any validation will wait
if (Operator == null)
{
RightStaticValue = staticValue;
return;
}
// If the left path is valid we can expect a type
Type leftSideType = LeftPath.GetPropertyType()!;
// If the operator does not support a right side, always set it to null
if (Operator.RightSideType == null)
{
RightStaticValue = null;
return;
}
// If not null ensure the types match and if not, convert it
if (staticValue != null && staticValue.GetType() == leftSideType)
if (staticValue != null && staticValue.GetType() == Operator.RightSideType)
RightStaticValue = staticValue;
else if (staticValue != null)
RightStaticValue = Convert.ChangeType(staticValue, leftSideType);
RightStaticValue = Convert.ChangeType(staticValue, Operator.RightSideType);
// If null create a default instance for value types or simply make it null for reference types
else if (leftSideType.IsValueType)
RightStaticValue = Activator.CreateInstance(leftSideType);
else if (Operator.RightSideType.IsValueType)
RightStaticValue = Activator.CreateInstance(Operator.RightSideType);
else
RightStaticValue = null;
}
@ -130,9 +136,8 @@ namespace Artemis.Core
/// Updates the operator of the predicate and re-compiles the expression
/// </summary>
/// <param name="conditionOperator"></param>
public void UpdateOperator(ConditionOperator? conditionOperator)
public void UpdateOperator(BaseConditionOperator? conditionOperator)
{
// Calling CreateExpression will clear compiled expressions
if (conditionOperator == null)
{
Operator = null;
@ -147,11 +152,13 @@ namespace Artemis.Core
}
Type leftType = LeftPath.GetPropertyType()!;
if (!conditionOperator.SupportsType(leftType))
// Left side can't go empty so enforce a match
if (!conditionOperator.SupportsType(leftType, ConditionParameterSide.Left))
throw new ArtemisCoreException($"Cannot apply operator {conditionOperator.GetType().Name} to this predicate because " +
$"it does not support left side type {leftType.Name}");
Operator = conditionOperator;
ValidateRightSide();
}
/// <inheritdoc />
@ -167,14 +174,14 @@ namespace Artemis.Core
if (leftSideValue != null && leftSideValue.GetType().IsValueType && RightStaticValue == null)
return false;
return Operator.Evaluate(leftSideValue, RightStaticValue);
return Operator.InternalEvaluate(leftSideValue, RightStaticValue);
}
if (RightPath == null || !RightPath.IsValid)
return false;
// Compare with dynamic values
return Operator.Evaluate(LeftPath.GetValue(), RightPath.GetValue());
return Operator.InternalEvaluate(LeftPath.GetValue(), RightPath.GetValue());
}
/// <inheritdoc />
@ -237,7 +244,7 @@ namespace Artemis.Core
// Operator
if (Entity.OperatorPluginGuid != null)
{
ConditionOperator? conditionOperator = ConditionOperatorStore.Get(Entity.OperatorPluginGuid.Value, Entity.OperatorType)?.ConditionOperator;
BaseConditionOperator? conditionOperator = ConditionOperatorStore.Get(Entity.OperatorPluginGuid.Value, Entity.OperatorType)?.ConditionOperator;
if (conditionOperator != null)
UpdateOperator(conditionOperator);
}
@ -292,28 +299,31 @@ namespace Artemis.Core
return;
Type leftType = LeftPath.GetPropertyType()!;
if (!Operator.SupportsType(leftType))
if (!Operator.SupportsType(leftType, ConditionParameterSide.Left))
Operator = null;
}
private void ValidateRightSide()
{
Type? leftType = LeftPath?.GetPropertyType();
if (Operator == null)
return;
if (PredicateType == ProfileRightSideType.Dynamic)
{
if (RightPath == null || !RightPath.IsValid)
return;
Type rightSideType = RightPath.GetPropertyType()!;
if (leftType != null && !leftType.IsCastableFrom(rightSideType))
if (!Operator.SupportsType(rightSideType, ConditionParameterSide.Right))
UpdateRightSideDynamic(null);
}
else
{
if (RightStaticValue != null && (leftType == null || leftType.IsCastableFrom(RightStaticValue.GetType())))
UpdateRightSideStatic(RightStaticValue);
else
UpdateRightSideStatic(null);
if (RightStaticValue == null)
return;
if (!Operator.SupportsType(RightStaticValue.GetType(), ConditionParameterSide.Right))
UpdateRightSideDynamic(null);
}
}
@ -321,7 +331,7 @@ namespace Artemis.Core
private void ConditionOperatorStoreOnConditionOperatorAdded(object? sender, ConditionOperatorStoreEvent e)
{
ConditionOperator conditionOperator = e.Registration.ConditionOperator;
BaseConditionOperator conditionOperator = e.Registration.ConditionOperator;
if (Entity.OperatorPluginGuid == conditionOperator.PluginInfo.Guid && Entity.OperatorType == conditionOperator.GetType().Name)
UpdateOperator(conditionOperator);
}

View File

@ -12,7 +12,7 @@ namespace Artemis.Core.Services
RegisterBuiltInConditionOperators();
}
public ConditionOperatorRegistration RegisterConditionOperator(PluginInfo pluginInfo, ConditionOperator conditionOperator)
public ConditionOperatorRegistration RegisterConditionOperator(PluginInfo pluginInfo, BaseConditionOperator conditionOperator)
{
if (pluginInfo == null)
throw new ArgumentNullException(nameof(pluginInfo));
@ -30,12 +30,12 @@ namespace Artemis.Core.Services
ConditionOperatorStore.Remove(registration);
}
public List<ConditionOperator> GetConditionOperatorsForType(Type type)
public List<BaseConditionOperator> GetConditionOperatorsForType(Type type, ConditionParameterSide side)
{
return ConditionOperatorStore.GetForType(type).Select(r => r.ConditionOperator).ToList();
return ConditionOperatorStore.GetForType(type, side).Select(r => r.ConditionOperator).ToList();
}
public ConditionOperator GetConditionOperator(Guid operatorPluginGuid, string operatorType)
public BaseConditionOperator GetConditionOperator(Guid operatorPluginGuid, string operatorType)
{
return ConditionOperatorStore.Get(operatorPluginGuid, operatorType)?.ConditionOperator;
}
@ -47,6 +47,8 @@ namespace Artemis.Core.Services
RegisterConditionOperator(Constants.CorePluginInfo, new NotEqualConditionOperator());
// Numeric operators
RegisterConditionOperator(Constants.CorePluginInfo, new NumberEqualsConditionOperator());
RegisterConditionOperator(Constants.CorePluginInfo, new NumberNotEqualConditionOperator());
RegisterConditionOperator(Constants.CorePluginInfo, new LessThanConditionOperator());
RegisterConditionOperator(Constants.CorePluginInfo, new GreaterThanConditionOperator());
RegisterConditionOperator(Constants.CorePluginInfo, new LessThanOrEqualConditionOperator());

View File

@ -14,7 +14,7 @@ namespace Artemis.Core.Services
/// </summary>
/// <param name="pluginInfo">The PluginInfo of the plugin this condition operator belongs to</param>
/// <param name="conditionOperator">The condition operator to register</param>
ConditionOperatorRegistration RegisterConditionOperator([NotNull] PluginInfo pluginInfo, [NotNull] ConditionOperator conditionOperator);
ConditionOperatorRegistration RegisterConditionOperator([NotNull] PluginInfo pluginInfo, [NotNull] BaseConditionOperator conditionOperator);
/// <summary>
/// Removes a condition operator so it is no longer available for use in layer conditions
@ -25,13 +25,13 @@ namespace Artemis.Core.Services
/// <summary>
/// Returns all the condition operators compatible with the provided type
/// </summary>
List<ConditionOperator> GetConditionOperatorsForType(Type type);
List<BaseConditionOperator> GetConditionOperatorsForType(Type type, ConditionParameterSide side);
/// <summary>
/// Gets a condition operator by its plugin GUID and type name
/// </summary>
/// <param name="operatorPluginGuid">The operator's plugin GUID</param>
/// <param name="operatorType">The type name of the operator</param>
ConditionOperator GetConditionOperator(Guid operatorPluginGuid, string operatorType);
BaseConditionOperator GetConditionOperator(Guid operatorPluginGuid, string operatorType);
}
}

View File

@ -8,7 +8,7 @@ namespace Artemis.Core
{
private static readonly List<ConditionOperatorRegistration> Registrations = new List<ConditionOperatorRegistration>();
public static ConditionOperatorRegistration Add(ConditionOperator conditionOperator)
public static ConditionOperatorRegistration Add(BaseConditionOperator conditionOperator)
{
ConditionOperatorRegistration registration;
lock (Registrations)
@ -46,19 +46,21 @@ namespace Artemis.Core
}
}
public static List<ConditionOperatorRegistration> GetForType(Type type)
public static List<ConditionOperatorRegistration> GetForType(Type type, ConditionParameterSide side)
{
lock (Registrations)
{
if (type == null)
return new List<ConditionOperatorRegistration>(Registrations);
List<ConditionOperatorRegistration> candidates = Registrations.Where(r => r.ConditionOperator.CompatibleTypes.Any(t => t.IsCastableFrom(type))).ToList();
List<ConditionOperatorRegistration> candidates = Registrations.Where(r => r.ConditionOperator.SupportsType(type, side)).ToList();
// If there are multiple operators with the same description, use the closest match
foreach (IGrouping<string, ConditionOperatorRegistration> candidate in candidates.GroupBy(r => r.ConditionOperator.Description).Where(g => g.Count() > 1).ToList())
{
ConditionOperatorRegistration closest = candidate.OrderByDescending(r => r.ConditionOperator.CompatibleTypes.Contains(type)).FirstOrDefault();
ConditionOperatorRegistration closest = side == ConditionParameterSide.Left
? candidate.OrderByDescending(r => r.ConditionOperator.LeftSideType.ScoreCastability(type)).First()
: candidate.OrderByDescending(r => r.ConditionOperator.RightSideType!.ScoreCastability(type)).First();
foreach (ConditionOperatorRegistration conditionOperator in candidate)
{
if (conditionOperator != closest)

View File

@ -7,7 +7,7 @@ namespace Artemis.Core
/// </summary>
public class ConditionOperatorRegistration
{
internal ConditionOperatorRegistration(ConditionOperator conditionOperator, Plugin plugin)
internal ConditionOperatorRegistration(BaseConditionOperator conditionOperator, Plugin plugin)
{
ConditionOperator = conditionOperator;
Plugin = plugin;
@ -18,7 +18,7 @@ namespace Artemis.Core
/// <summary>
/// Gets the condition operator that has been registered
/// </summary>
public ConditionOperator ConditionOperator { get; }
public BaseConditionOperator ConditionOperator { get; }
/// <summary>
/// Gets the plugin the condition operator is associated with

View File

@ -21,10 +21,10 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
private readonly IProfileEditorService _profileEditorService;
private bool _isPrimitiveList;
private DataModelDynamicViewModel _leftSideSelectionViewModel;
private BindableCollection<ConditionOperator> _operators;
private BindableCollection<BaseConditionOperator> _operators;
private DataModelStaticViewModel _rightSideInputViewModel;
private DataModelDynamicViewModel _rightSideSelectionViewModel;
private ConditionOperator _selectedOperator;
private BaseConditionOperator _selectedOperator;
private List<Type> _supportedInputTypes;
@ -40,14 +40,14 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
_supportedInputTypes = new List<Type>();
SelectOperatorCommand = new DelegateCommand(ExecuteSelectOperatorCommand);
Operators = new BindableCollection<ConditionOperator>();
Operators = new BindableCollection<BaseConditionOperator>();
Initialize();
}
public DataModelConditionListPredicate DataModelConditionListPredicate => (DataModelConditionListPredicate) Model;
public BindableCollection<ConditionOperator> Operators
public BindableCollection<BaseConditionOperator> Operators
{
get => _operators;
set => SetAndNotify(ref _operators, value);
@ -71,7 +71,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
set => SetAndNotify(ref _rightSideInputViewModel, value);
}
public ConditionOperator SelectedOperator
public BaseConditionOperator SelectedOperator
{
get => _selectedOperator;
set => SetAndNotify(ref _selectedOperator, value);
@ -129,35 +129,41 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
// Get the supported operators
Operators.Clear();
Operators.AddRange(_conditionOperatorService.GetConditionOperatorsForType(leftSideType ?? typeof(object)));
Operators.AddRange(_conditionOperatorService.GetConditionOperatorsForType(leftSideType ?? typeof(object), ConditionParameterSide.Left));
if (DataModelConditionListPredicate.Operator == null)
DataModelConditionListPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.SupportsType(leftSideType ?? typeof(object))));
DataModelConditionListPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.SupportsType(leftSideType ?? typeof(object), ConditionParameterSide.Left)));
SelectedOperator = DataModelConditionListPredicate.Operator;
if (SelectedOperator == null || !SelectedOperator.SupportsRightSide)
// Without a selected operator or one that supports a right side, leave the right side input empty
if (SelectedOperator == null || SelectedOperator.RightSideType == null)
{
DisposeRightSideStaticViewModel();
DisposeRightSideDynamicViewModel();
return;
}
// Ensure the right side has the proper VM
if (DataModelConditionListPredicate.PredicateType == ProfileRightSideType.Dynamic && SelectedOperator.SupportsRightSide)
if (DataModelConditionListPredicate.PredicateType == ProfileRightSideType.Dynamic)
{
DisposeRightSideStaticViewModel();
if (RightSideSelectionViewModel == null)
CreateRightSideSelectionViewModel();
RightSideSelectionViewModel.FilterTypes = new[] {leftSideType};
RightSideSelectionViewModel.ChangeDataModelPath(DataModelConditionListPredicate.RightPath);
RightSideSelectionViewModel.FilterTypes = new[] {SelectedOperator.RightSideType};
}
else if (SelectedOperator.SupportsRightSide)
else
{
DisposeRightSideDynamicViewModel();
if (RightSideInputViewModel == null)
CreateRightSideInputViewModel(leftSideType);
CreateRightSideInputViewModel(SelectedOperator.RightSideType);
RightSideInputViewModel.Value = DataModelConditionListPredicate.RightStaticValue;
if (RightSideInputViewModel.TargetType != leftSideType)
RightSideInputViewModel.UpdateTargetType(leftSideType);
if (SelectedOperator.RightSideType.IsValueType && DataModelConditionListPredicate.RightStaticValue == null)
RightSideInputViewModel.Value = SelectedOperator.RightSideType.GetDefault();
else
RightSideInputViewModel.Value = DataModelConditionListPredicate.RightStaticValue;
if (RightSideInputViewModel.TargetType != SelectedOperator.RightSideType)
RightSideInputViewModel.UpdateTargetType(SelectedOperator.RightSideType);
}
}
@ -210,7 +216,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
private void ExecuteSelectOperatorCommand(object context)
{
if (!(context is ConditionOperator dataModelConditionOperator))
if (!(context is BaseConditionOperator dataModelConditionOperator))
return;
SelectedOperator = dataModelConditionOperator;
@ -276,7 +282,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
// Add an extra data model to the selection VM to allow self-referencing the current item
// The safe cast prevents adding this extra VM on primitive lists where they serve no purpose
if (GetListDataModel()?.Children?.FirstOrDefault() is DataModelPropertiesViewModel listValue)
if (GetListDataModel()?.Children?.FirstOrDefault() is DataModelPropertiesViewModel listValue)
RightSideSelectionViewModel.ExtraDataModelViewModels.Add(listValue);
}

View File

@ -19,10 +19,10 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
private readonly IDataModelUIService _dataModelUIService;
private readonly IProfileEditorService _profileEditorService;
private DataModelDynamicViewModel _leftSideSelectionViewModel;
private BindableCollection<ConditionOperator> _operators;
private BindableCollection<BaseConditionOperator> _operators;
private DataModelStaticViewModel _rightSideInputViewModel;
private DataModelDynamicViewModel _rightSideSelectionViewModel;
private ConditionOperator _selectedOperator;
private BaseConditionOperator _selectedOperator;
private List<Type> _supportedInputTypes;
@ -39,7 +39,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
_supportedInputTypes = new List<Type>();
SelectOperatorCommand = new DelegateCommand(ExecuteSelectOperatorCommand);
Operators = new BindableCollection<ConditionOperator>();
Operators = new BindableCollection<BaseConditionOperator>();
ShowDataModelValues = settingsService.GetSetting<bool>("ProfileEditor.ShowDataModelValues");
@ -50,7 +50,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
public PluginSetting<bool> ShowDataModelValues { get; }
public BindableCollection<ConditionOperator> Operators
public BindableCollection<BaseConditionOperator> Operators
{
get => _operators;
set => SetAndNotify(ref _operators, value);
@ -62,7 +62,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
set => SetAndNotify(ref _leftSideSelectionViewModel, value);
}
public ConditionOperator SelectedOperator
public BaseConditionOperator SelectedOperator
{
get => _selectedOperator;
set => SetAndNotify(ref _selectedOperator, value);
@ -109,12 +109,13 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
// Get the supported operators
Operators.Clear();
Operators.AddRange(_conditionOperatorService.GetConditionOperatorsForType(leftSideType ?? typeof(object)));
Operators.AddRange(_conditionOperatorService.GetConditionOperatorsForType(leftSideType ?? typeof(object), ConditionParameterSide.Left));
if (DataModelConditionPredicate.Operator == null)
DataModelConditionPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.SupportsType(leftSideType ?? typeof(object))));
DataModelConditionPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.SupportsType(leftSideType ?? typeof(object), ConditionParameterSide.Left)));
SelectedOperator = DataModelConditionPredicate.Operator;
if (SelectedOperator == null || !SelectedOperator.SupportsRightSide)
// Without a selected operator or one that supports a right side, leave the right side input empty
if (SelectedOperator == null || SelectedOperator.RightSideType == null)
{
DisposeRightSideStaticViewModel();
DisposeRightSideDynamicViewModel();
@ -129,20 +130,20 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
CreateRightSideSelectionViewModel();
RightSideSelectionViewModel.ChangeDataModelPath(DataModelConditionPredicate.RightPath);
RightSideSelectionViewModel.FilterTypes = new[] {leftSideType};
RightSideSelectionViewModel.FilterTypes = new[] {SelectedOperator.RightSideType};
}
else
{
DisposeRightSideDynamicViewModel();
if (RightSideInputViewModel == null)
CreateRightSideInputViewModel(leftSideType);
CreateRightSideInputViewModel(SelectedOperator.RightSideType);
if (leftSideType != null && leftSideType.IsValueType && DataModelConditionPredicate.RightStaticValue == null)
RightSideInputViewModel.Value = leftSideType.GetDefault();
if (SelectedOperator.RightSideType.IsValueType && DataModelConditionPredicate.RightStaticValue == null)
RightSideInputViewModel.Value = SelectedOperator.RightSideType.GetDefault();
else
RightSideInputViewModel.Value = DataModelConditionPredicate.RightStaticValue;
if (RightSideInputViewModel.TargetType != leftSideType)
RightSideInputViewModel.UpdateTargetType(leftSideType);
if (RightSideInputViewModel.TargetType != SelectedOperator.RightSideType)
RightSideInputViewModel.UpdateTargetType(SelectedOperator.RightSideType);
}
}
@ -188,7 +189,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
private void ExecuteSelectOperatorCommand(object context)
{
if (!(context is ConditionOperator DataModelConditionOperator))
if (!(context is BaseConditionOperator DataModelConditionOperator))
return;
SelectedOperator = DataModelConditionOperator;