chore: initial git load of code space
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
using System.Globalization;
|
||||
using Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
namespace Strata.SqlTools.Rules;
|
||||
|
||||
public interface IVisitor<out T>
|
||||
{
|
||||
T VisitParameter(Parameter parameter);
|
||||
T VisitProperty(Property property);
|
||||
T VisitCollectionProperty(CollectionProperty collectionProperty);
|
||||
|
||||
T VisitAny(Any Any);
|
||||
|
||||
T VisitLiteral(Literal literalRule);
|
||||
|
||||
T VisitEquals(Equal Equal);
|
||||
T VisitNotEquals(NotEqual Equal);
|
||||
T VisitGreaterThan(GreaterThan GreaterThan);
|
||||
|
||||
T VisitAnd(And And);
|
||||
T VisitOr(Or Or);
|
||||
T VisitWith(With With);
|
||||
}
|
||||
|
||||
public abstract class Visitor : IVisitor<Expression>
|
||||
{
|
||||
public virtual Expression Visit(IVisitable expression) => expression.Accept(this);
|
||||
|
||||
public virtual Expression VisitParameter(Parameter parameter) => parameter;
|
||||
|
||||
public virtual Expression VisitProperty(Property property) => property;
|
||||
|
||||
public virtual Expression VisitCollectionProperty(CollectionProperty collectionProperty) => collectionProperty;
|
||||
|
||||
public virtual Expression VisitAny(Any Any) => new Any(
|
||||
(CollectionProperty)Visit(Any.CollectionProperty),
|
||||
(BoolExpr)Visit(Any.BoolExpr),
|
||||
(Parameter)Visit(Any.PredicateParameter));
|
||||
|
||||
public virtual Expression VisitLiteral(Literal literalRule) => literalRule;
|
||||
|
||||
public virtual Expression VisitEquals(Equal Equal) => Validate(Equal);
|
||||
|
||||
public virtual Expression VisitNotEquals(NotEqual notEqual) => Validate(notEqual);
|
||||
|
||||
public virtual Expression VisitGreaterThan(GreaterThan GreaterThan) => Validate(GreaterThan);
|
||||
|
||||
public virtual Expression VisitAnd(And And) =>
|
||||
new And((BoolExpr)Visit(And.Left), (BoolExpr)Visit(And.Right));
|
||||
|
||||
public virtual Expression VisitOr(Or Or) =>
|
||||
new Or((BoolExpr)Visit(Or.Left), (BoolExpr)Visit(Or.Right));
|
||||
|
||||
public virtual Expression VisitWith(With With) =>
|
||||
new With((BoolExpr)Visit(With.Left), (BoolExpr)Visit(With.Right));
|
||||
|
||||
protected virtual Expression Validate(Comparison comparison)
|
||||
{
|
||||
return comparison.Update(Visit(comparison.Left), Visit(comparison.Right));
|
||||
}
|
||||
}
|
||||
|
||||
public class LocalVisitor : IVisitor<string>
|
||||
{
|
||||
public virtual string Visit(IVisitable expression) => expression.Accept(this);
|
||||
|
||||
public virtual string VisitLiteral(Literal literalRule) => literalRule switch
|
||||
{
|
||||
NumberLiteral number => number.Value.ToString(CultureInfo.InvariantCulture),
|
||||
StringLiteral stringRule => $"\"{stringRule.Value}\"",
|
||||
not null => literalRule.Value.ToString() ?? string.Empty,
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
public virtual string VisitEquals(Equal Equal)
|
||||
{
|
||||
return $"{Equal.Left.Accept(this)} == {Equal.Right.Accept(this)}";
|
||||
}
|
||||
|
||||
public virtual string VisitNotEquals(NotEqual notEqual)
|
||||
{
|
||||
return $"{notEqual.Left.Accept(this)} != {notEqual.Right.Accept(this)}";
|
||||
}
|
||||
|
||||
public virtual string VisitGreaterThan(GreaterThan GreaterThan)
|
||||
{
|
||||
return $"{GreaterThan.Left.Accept(this)} > {GreaterThan.Right.Accept(this)}";
|
||||
}
|
||||
|
||||
public virtual string VisitAnd(And And)
|
||||
{
|
||||
return $"{And.Left.Accept(this)} && {And.Right.Accept(this)}";
|
||||
}
|
||||
|
||||
public virtual string VisitOr(Or Or)
|
||||
{
|
||||
return $"{Or.Left.Accept(this)} || {Or.Right.Accept(this)}";
|
||||
}
|
||||
|
||||
public virtual string VisitWith(With With)
|
||||
{
|
||||
// just converting it to an AND expression for now
|
||||
var and = new And(With.Left, With.Right);
|
||||
return and.Accept(this);
|
||||
//throw new NotImplementedException("not sure what to do with 'WITH' expressions yet");
|
||||
}
|
||||
|
||||
private bool TryGetCollectionItemProperty(Expression Expression, out Property? property)
|
||||
{
|
||||
property = null;
|
||||
|
||||
if (Expression is not IBinary binary)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binary.Left is not Property Property)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Property.Expression is not CollectionProperty collection)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
property = Property;
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual string VisitParameter(Parameter parameter) => $"{parameter.ParameterName}";
|
||||
|
||||
public virtual string VisitProperty(Property property)
|
||||
{
|
||||
return property.Expression is null
|
||||
? $"{property.PropertyName}"
|
||||
: $"{property.Expression.Accept(this)}.{property.PropertyName}";
|
||||
}
|
||||
|
||||
public virtual string VisitCollectionProperty(CollectionProperty collectionProperty) => VisitProperty(collectionProperty);
|
||||
|
||||
public virtual string VisitAny(Any Any)
|
||||
{
|
||||
return $"{Any.CollectionProperty.Accept(this)}.Any({Any.PredicateParameter.Accept(this)} => {Any.BoolExpr.Accept(this)})";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a logical AND operation between two BoolExpr expressions.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Left} AND {Right}")]
|
||||
public class And : Logical
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="And"/> class.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
public And(BoolExpr left, BoolExpr right) : base(left, right) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitAnd(this);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an ANY expression that checks if any element in a collection satisfies a condition.
|
||||
/// </summary>
|
||||
public class Any : BoolExpr
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the collection property being evaluated.
|
||||
/// </summary>
|
||||
public CollectionProperty CollectionProperty { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the BoolExpr expression that defines the condition to check.
|
||||
/// </summary>
|
||||
public BoolExpr BoolExpr { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter used in the predicate expression.
|
||||
/// </summary>
|
||||
public Parameter PredicateParameter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Any"/> class with a function.
|
||||
/// </summary>
|
||||
/// <param name="collectionProperty">The collection property to evaluate.</param>
|
||||
/// <param name="func">A function that defines the condition to check for each element.</param>
|
||||
public Any(CollectionProperty collectionProperty, Func<Parameter, BoolExpr> func)
|
||||
{
|
||||
CollectionProperty = collectionProperty;
|
||||
PredicateParameter = new Parameter("p");
|
||||
BoolExpr = func(PredicateParameter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Any"/> class with a BoolExpr expression.
|
||||
/// </summary>
|
||||
/// <param name="collectionProperty">The collection property to evaluate.</param>
|
||||
/// <param name="boolExpr">The BoolExpr expression defining the condition.</param>
|
||||
public Any(CollectionProperty collectionProperty, BoolExpr boolExpr)
|
||||
: this(collectionProperty, boolExpr, new Parameter("p"))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Any"/> class.
|
||||
/// </summary>
|
||||
/// <param name="collectionProperty">The collection property to evaluate.</param>
|
||||
/// <param name="boolExpr">The BoolExpr expression defining the condition.</param>
|
||||
/// <param name="predicateParameter">The parameter used in the predicate expression.</param>
|
||||
public Any(CollectionProperty collectionProperty, BoolExpr boolExpr, Parameter predicateParameter)
|
||||
{
|
||||
CollectionProperty = collectionProperty;
|
||||
BoolExpr = boolExpr;
|
||||
PredicateParameter = predicateParameter;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitAny(this);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an expression that evaluates to a BoolExpr value (true/false).
|
||||
/// </summary>
|
||||
public abstract class BoolExpr : Expression
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a logical AND expression combining two BoolExpr expressions.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
/// <returns>An AND expression combining both operands.</returns>
|
||||
public static BoolExpr operator &(BoolExpr left, BoolExpr right) => new And(left, right);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a logical OR expression combining two BoolExpr expressions.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
/// <returns>An OR expression combining both operands.</returns>
|
||||
public static BoolExpr operator |(BoolExpr left, BoolExpr right) => new Or(left, right);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a collection property access in a rule expression.
|
||||
/// </summary>
|
||||
public class CollectionProperty : Property
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CollectionProperty"/> class.
|
||||
/// </summary>
|
||||
/// <param name="expression">The containing expression.</param>
|
||||
/// <param name="propertyName">The name of the collection property.</param>
|
||||
public CollectionProperty(Expression? expression, string propertyName) : base(expression, propertyName)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitCollectionProperty(this);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an ANY expression that checks if any element in the collection satisfies a condition.
|
||||
/// </summary>
|
||||
/// <param name="func">A function that defines the condition to check for each element.</param>
|
||||
/// <returns>An ANY expression.</returns>
|
||||
public Any Any(Func<Parameter, BoolExpr> func)
|
||||
{
|
||||
var parameter = new Parameter("p");
|
||||
var BoolExpr = func(parameter);
|
||||
return new Any(this, BoolExpr, parameter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a comparison operation between two expressions.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Left} {Type} {Right}")]
|
||||
public abstract class Comparison : BoolExpr, IBinary
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the left operand of the comparison.
|
||||
/// </summary>
|
||||
public Expression Left { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the right operand of the comparison.
|
||||
/// </summary>
|
||||
public Expression Right { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of comparison operation.
|
||||
/// </summary>
|
||||
public abstract Type Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Comparison"/> class.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
protected Comparison(Expression left, Expression right)
|
||||
{
|
||||
Left = left;
|
||||
Right = right;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new comparison expression with updated operands.
|
||||
/// </summary>
|
||||
/// <param name="left">The new left operand.</param>
|
||||
/// <param name="right">The new right operand.</param>
|
||||
/// <returns>A new comparison expression or this instance if operands are unchanged.</returns>
|
||||
public Expression Update(Expression left, Expression right)
|
||||
{
|
||||
if (ReferenceEquals(left, Left) && ReferenceEquals(right, Right))
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
return Create(left, right, Type);
|
||||
}
|
||||
|
||||
private static Comparison Create(Expression left, Expression right, Type Type)
|
||||
{
|
||||
return Type switch
|
||||
{
|
||||
Type.Equal => new Equal(left, right),
|
||||
Type.NotEqual => new NotEqual(left, right),
|
||||
Type.GreaterThan => new GreaterThan(left, right),
|
||||
|
||||
_ => throw new NotImplementedException("not yet")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an equality comparison between two expressions.
|
||||
/// </summary>
|
||||
public class Equal : Comparison
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override Type Type => Type.Equal;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Equal"/> class.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
public Equal(Expression left, Expression right) : base(left, right) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitEquals(this);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Provides implicit conversion operators and comparison operators for rule expressions.
|
||||
/// </summary>
|
||||
#pragma warning disable CS0660, CS0661
|
||||
public partial class Expression
|
||||
#pragma warning restore CS0660, CS0661
|
||||
{
|
||||
/// <summary>
|
||||
/// Implicitly converts a decimal value to a rule expression.
|
||||
/// </summary>
|
||||
/// <param name="value">The decimal value to convert.</param>
|
||||
public static implicit operator Expression(decimal value) => new NumberLiteral(value);
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts a string value to a rule expression.
|
||||
/// </summary>
|
||||
/// <param name="value">The string value to convert.</param>
|
||||
public static implicit operator Expression(string value) => new StringLiteral(value);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an equality comparison rule expression.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
/// <returns>An equality comparison rule expression.</returns>
|
||||
public static Comparison operator ==(Expression left, Expression right) => new Equal(left, right);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a not-equal comparison rule expression.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
/// <returns>A not-equal comparison rule expression.</returns>
|
||||
public static Comparison operator !=(Expression left, Expression right) => new NotEqual(left, right);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an equality comparison rule expression.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
/// <returns>An equality comparison rule expression.</returns>
|
||||
public static Equal Equal(Expression left, Expression right) => new(left, right);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all rule expressions.
|
||||
/// </summary>
|
||||
#pragma warning disable CS0660, CS0661
|
||||
public abstract partial class Expression : IVisitable
|
||||
#pragma warning restore CS0660, CS0661
|
||||
{
|
||||
/// <summary>
|
||||
/// Accepts a visitor and allows it to process this rule expression.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type of the visitor.</typeparam>
|
||||
/// <param name="visitor">The visitor to accept.</param>
|
||||
/// <returns>The result of the visitor's processing.</returns>
|
||||
public abstract T Accept<T>(IVisitor<T> visitor);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a greater-than comparison between two expressions.
|
||||
/// </summary>
|
||||
public class GreaterThan : Comparison
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override Type Type => Type.GreaterThan;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GreaterThan"/> class.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
public GreaterThan(Expression left, Expression right) : base(left, right) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitGreaterThan(this);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a binary rule expression with left and right operands.
|
||||
/// </summary>
|
||||
public interface IBinary : IVisitable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the left operand.
|
||||
/// </summary>
|
||||
public Expression Left { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the right operand.
|
||||
/// </summary>
|
||||
public Expression Right { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of rule expression.
|
||||
/// </summary>
|
||||
public Type Type { get; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an object that can be visited by a rule visitor implementing the visitor pattern.
|
||||
/// </summary>
|
||||
public interface IVisitable
|
||||
{
|
||||
/// <summary>
|
||||
/// Accepts a visitor and allows it to process this visitable object.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type of the visitor.</typeparam>
|
||||
/// <param name="visitor">The visitor to accept.</param>
|
||||
/// <returns>The result of the visitor's processing.</returns>
|
||||
T Accept<T>(IVisitor<T> visitor);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a literal value in a rule expression.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("\\{{Value}\\}")]
|
||||
public class Literal : Expression
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the literal value.
|
||||
/// </summary>
|
||||
public object Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Literal"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The literal value.</param>
|
||||
public Literal(object value) => Value = value;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitLiteral(this);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for typed literal rule expressions.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type of the literal value.</typeparam>
|
||||
public abstract class Literal<TValue> : Literal
|
||||
where TValue : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the strongly-typed literal value.
|
||||
/// </summary>
|
||||
public new TValue Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Literal{TValue}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The literal value.</param>
|
||||
protected Literal(TValue value) : base(value) => Value = value;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a logical operation on two BoolExpr input expressions (e.g., AND, OR).
|
||||
/// </summary>
|
||||
public abstract class Logical : BoolExpr
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the left operand of the logical expression.
|
||||
/// </summary>
|
||||
public BoolExpr Left { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the right operand of the logical expression.
|
||||
/// </summary>
|
||||
public BoolExpr Right { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Logical"/> class.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
protected Logical(BoolExpr left, BoolExpr right)
|
||||
{
|
||||
Left = left;
|
||||
Right = right;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Parses markdown/LaTeX mathematical expressions and converts them to Expression objects.
|
||||
/// Supports parsing of logical operations, comparisons, properties, and literals.
|
||||
/// </summary>
|
||||
public static class Markdown
|
||||
{
|
||||
private static readonly Dictionary<string, Func<Expression, Expression, BoolExpr>> LogicalOperators = new()
|
||||
{
|
||||
{ "\\land", (left, right) => new And((BoolExpr)left, (BoolExpr)right) },
|
||||
{ "\\lor", (left, right) => new Or((BoolExpr)left, (BoolExpr)right) },
|
||||
{ "\\wedge", (left, right) => new And((BoolExpr)left, (BoolExpr)right) },
|
||||
{ "\\vee", (left, right) => new Or((BoolExpr)left, (BoolExpr)right) },
|
||||
{ "AND", (left, right) => new And((BoolExpr)left, (BoolExpr)right) },
|
||||
{ "OR", (left, right) => new Or((BoolExpr)left, (BoolExpr)right) },
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, Func<Expression, Expression, Comparison>> ComparisonOperators = new()
|
||||
{
|
||||
{ "=", (left, right) => new Equal(left, right) },
|
||||
{ "\\neq", (left, right) => new NotEqual(left, right) },
|
||||
{ "!=", (left, right) => new NotEqual(left, right) },
|
||||
{ ">", (left, right) => new GreaterThan(left, right) },
|
||||
{ "\\gt", (left, right) => new GreaterThan(left, right) },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Parses a markdown/LaTeX string into an Expression object.
|
||||
/// </summary>
|
||||
/// <param name="markdown">The markdown/LaTeX string to parse.</param>
|
||||
/// <returns>The parsed Expression object.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the markdown cannot be parsed.</exception>
|
||||
public static Expression Parse(string markdown)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(markdown))
|
||||
{
|
||||
throw new ArgumentException("Markdown cannot be null or empty", nameof(markdown));
|
||||
}
|
||||
|
||||
// Remove common markdown delimiters
|
||||
markdown = markdown.Trim();
|
||||
markdown = StripMarkdownDelimiters(markdown);
|
||||
|
||||
return ParseExpression(markdown);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a markdown/LaTeX string into an Expression object.
|
||||
/// </summary>
|
||||
/// <param name="markdown">The markdown/LaTeX string to parse.</param>
|
||||
/// <param name="expression">The parsed Expression object if successful.</param>
|
||||
/// <returns>True if parsing was successful, false otherwise.</returns>
|
||||
public static bool TryParse(string markdown, out Expression? expression)
|
||||
{
|
||||
try
|
||||
{
|
||||
expression = Parse(markdown);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
expression = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string StripMarkdownDelimiters(string text)
|
||||
{
|
||||
// Remove $...$ or $$...$$ delimiters
|
||||
text = Regex.Replace(text, @"^\$\$?\s*", "");
|
||||
text = Regex.Replace(text, @"\s*\$\$?$", "");
|
||||
|
||||
// Remove ```math...``` code fence
|
||||
text = Regex.Replace(text, @"^```math\s*", "", RegexOptions.Multiline);
|
||||
text = Regex.Replace(text, @"\s*```$", "", RegexOptions.Multiline);
|
||||
|
||||
return text.Trim();
|
||||
}
|
||||
|
||||
private static Expression ParseExpression(string text)
|
||||
{
|
||||
text = text.Trim();
|
||||
|
||||
// Try to parse logical operations (lowest precedence)
|
||||
var logicalExpr = TryParseLogicalOperation(text);
|
||||
if (logicalExpr is not null)
|
||||
{
|
||||
return logicalExpr;
|
||||
}
|
||||
|
||||
// Try to parse comparison operations
|
||||
var comparisonExpr = TryParseComparison(text);
|
||||
if (comparisonExpr is not null)
|
||||
{
|
||||
return comparisonExpr;
|
||||
}
|
||||
|
||||
// Handle parentheses
|
||||
var parenthesisExpr = TryParseParentheses(text);
|
||||
if (parenthesisExpr is not null)
|
||||
{
|
||||
return parenthesisExpr;
|
||||
}
|
||||
|
||||
// Parse property, literal, or other atomic expressions
|
||||
return ParseAtomicExpression(text);
|
||||
}
|
||||
|
||||
private static Expression? TryParseLogicalOperation(string text)
|
||||
{
|
||||
foreach (var op in LogicalOperators.Keys)
|
||||
{
|
||||
var parts = SplitByOperator(text, op);
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
var left = ParseExpression(parts[0]);
|
||||
var right = ParseExpression(parts[1]);
|
||||
return LogicalOperators[op](left, right);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Expression? TryParseComparison(string text)
|
||||
{
|
||||
foreach (var op in ComparisonOperators.Keys)
|
||||
{
|
||||
var parts = SplitByOperator(text, op);
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
var left = ParseExpression(parts[0]);
|
||||
var right = ParseExpression(parts[1]);
|
||||
return ComparisonOperators[op](left, right);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Expression? TryParseParentheses(string text)
|
||||
{
|
||||
// Handle regular parentheses
|
||||
if (text.StartsWith('(') && text.EndsWith(')'))
|
||||
{
|
||||
var inner = text.Substring(1, text.Length - 2);
|
||||
if (IsBalanced(inner))
|
||||
{
|
||||
return ParseExpression(inner);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle LaTeX \left( and \right)
|
||||
if (text.StartsWith("\\left(") && text.EndsWith("\\right)"))
|
||||
{
|
||||
var inner = text.Substring(6, text.Length - 13);
|
||||
if (IsBalanced(inner))
|
||||
{
|
||||
return ParseExpression(inner);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Expression ParseAtomicExpression(string text)
|
||||
{
|
||||
// Try parsing as property access
|
||||
var propertyExpr = TryParseProperty(text);
|
||||
if (propertyExpr is not null)
|
||||
{
|
||||
return propertyExpr;
|
||||
}
|
||||
|
||||
// Try parsing as literal
|
||||
var literalExpr = TryParseLiteral(text);
|
||||
if (literalExpr is not null)
|
||||
{
|
||||
return literalExpr;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unable to parse expression: {text}");
|
||||
}
|
||||
|
||||
private static Expression? TryParseProperty(string text)
|
||||
{
|
||||
// Parse property access (e.g., x.PropertyName or \text{x.PropertyName})
|
||||
var propertyMatch = Regex.Match(text, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$");
|
||||
if (propertyMatch.Success)
|
||||
{
|
||||
return new Property(propertyMatch.Groups[1].Value, propertyMatch.Groups[2].Value);
|
||||
}
|
||||
|
||||
// Parse \text{...} property access
|
||||
var textMatch = Regex.Match(text, @"^\\text\{([^}]+)\}$");
|
||||
if (textMatch.Success)
|
||||
{
|
||||
var textContent = textMatch.Groups[1].Value;
|
||||
var propMatch = Regex.Match(textContent, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$");
|
||||
if (propMatch.Success)
|
||||
{
|
||||
return new Property(propMatch.Groups[1].Value, propMatch.Groups[2].Value);
|
||||
}
|
||||
|
||||
// Check for boolean literals in \text{} format
|
||||
if (textContent.Equals("true", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Literal(true);
|
||||
}
|
||||
|
||||
if (textContent.Equals("false", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Literal(false);
|
||||
}
|
||||
|
||||
// Single property name
|
||||
if (Regex.IsMatch(textContent, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
|
||||
{
|
||||
return new Property(textContent);
|
||||
}
|
||||
|
||||
// String literal
|
||||
return new StringLiteral(textContent);
|
||||
}
|
||||
|
||||
// Check for boolean literals before simple property
|
||||
if (text.Equals("true", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Literal(true);
|
||||
}
|
||||
|
||||
if (text.Equals("false", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Literal(false);
|
||||
}
|
||||
|
||||
// Parse simple property without parameter
|
||||
if (Regex.IsMatch(text, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
|
||||
{
|
||||
return new Property(text);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Expression? TryParseLiteral(string text)
|
||||
{
|
||||
// Parse string literals (quoted)
|
||||
var stringMatch = Regex.Match(text, @"^[""'](.+?)[""']$");
|
||||
if (stringMatch.Success)
|
||||
{
|
||||
return new StringLiteral(stringMatch.Groups[1].Value);
|
||||
}
|
||||
|
||||
// Parse empty string literals
|
||||
if (text == "\"\"" || text == "''")
|
||||
{
|
||||
return new StringLiteral(string.Empty);
|
||||
}
|
||||
|
||||
// Parse numeric literals
|
||||
if (int.TryParse(text, out var intValue))
|
||||
{
|
||||
return new NumberLiteral(intValue);
|
||||
}
|
||||
|
||||
if (decimal.TryParse(text, out var decimalValue))
|
||||
{
|
||||
return new NumberLiteral(decimalValue);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string[] SplitByOperator(string text, string op)
|
||||
{
|
||||
var result = new List<string>();
|
||||
int depth = 0;
|
||||
int lastIndex = 0;
|
||||
int i = 0;
|
||||
|
||||
while (i < text.Length)
|
||||
{
|
||||
i = ProcessParentheses(text, i, ref depth);
|
||||
if (i >= text.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we found the operator at depth 0
|
||||
if (depth == 0 && i + op.Length <= text.Length && TryMatchOperator(text, i, op))
|
||||
{
|
||||
result.Add(text.Substring(lastIndex, i - lastIndex).Trim());
|
||||
lastIndex = i + op.Length;
|
||||
i += op.Length;
|
||||
continue;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
if (result.Count == 0)
|
||||
{
|
||||
return new[] { text };
|
||||
}
|
||||
|
||||
result.Add(text.Substring(lastIndex).Trim());
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private static int ProcessParentheses(string text, int index, ref int depth)
|
||||
{
|
||||
// Track parentheses depth
|
||||
if (text[index] == '(' || (index + 5 < text.Length && text.Substring(index, 6) == "\\left("))
|
||||
{
|
||||
depth++;
|
||||
if (text[index] == '\\')
|
||||
{
|
||||
return index + 6;
|
||||
}
|
||||
else
|
||||
{
|
||||
return index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (text[index] == ')' || (index + 6 < text.Length && text.Substring(index, 7) == "\\right)"))
|
||||
{
|
||||
depth--;
|
||||
if (text[index] == '\\')
|
||||
{
|
||||
return index + 7;
|
||||
}
|
||||
else
|
||||
{
|
||||
return index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private static bool TryMatchOperator(string text, int index, string op)
|
||||
{
|
||||
var substring = text.Substring(index, op.Length);
|
||||
if (substring != op)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure it's a separate operator, not part of a larger token
|
||||
bool validBefore = (index == 0 || char.IsWhiteSpace(text[index - 1]) || text[index] == '\\');
|
||||
bool validAfter = (index + op.Length >= text.Length || char.IsWhiteSpace(text[index + op.Length]));
|
||||
|
||||
return validBefore && validAfter;
|
||||
}
|
||||
|
||||
private static bool IsBalanced(string text)
|
||||
{
|
||||
int depth = 0;
|
||||
int i = 0;
|
||||
|
||||
while (i < text.Length)
|
||||
{
|
||||
if (text[i] == '(')
|
||||
{
|
||||
depth++;
|
||||
}
|
||||
else if (text[i] == ')')
|
||||
{
|
||||
depth--;
|
||||
if (depth < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (i + 5 < text.Length && text.Substring(i, 6) == "\\left(")
|
||||
{
|
||||
depth++;
|
||||
i += 5;
|
||||
}
|
||||
else if (i + 6 < text.Length && text.Substring(i, 7) == "\\right)")
|
||||
{
|
||||
depth--;
|
||||
if (depth < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
i += 6;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return depth == 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a not-equal comparison between two expressions.
|
||||
/// </summary>
|
||||
public class NotEqual : Comparison
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override Type Type => Type.NotEqual;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotEqual"/> class.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
public NotEqual(Expression left, Expression right) : base(left, right) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitNotEquals(this);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a numeric literal value in a rule expression.
|
||||
/// </summary>
|
||||
public class NumberLiteral : Literal<decimal>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref=" NumberLiteral"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The numeric value.</param>
|
||||
public NumberLiteral(decimal value) : base(value) { }
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts a <see cref=" NumberLiteral"/> to a decimal value.
|
||||
/// </summary>
|
||||
/// <param name="numberExp">The number expression to convert.</param>
|
||||
public static implicit operator decimal(NumberLiteral numberExp) => numberExp.Value;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a logical OR operation between two BoolExpr expressions.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Left} OR {Right}")]
|
||||
public class Or : Logical
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Or"/> class.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
public Or(BoolExpr left, BoolExpr right) : base(left, right) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitOr(this);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a parameter in a rule expression.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{ParameterName}")]
|
||||
public class Parameter : Expression
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the parameter.
|
||||
/// </summary>
|
||||
public string ParameterName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Parameter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The name of the parameter.</param>
|
||||
public Parameter(string parameterName) => ParameterName = parameterName;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitParameter(this);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a property expression for accessing a property on this parameter.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">The name of the property.</param>
|
||||
/// <returns>A property expression.</returns>
|
||||
public Property Property(string propertyName) => new(this, propertyName);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a collection property expression for accessing a collection property on this parameter.
|
||||
/// </summary>
|
||||
/// <param name="collectionPropertyName">The name of the collection property.</param>
|
||||
/// <returns>A collection property expression.</returns>
|
||||
public CollectionProperty CollectionProperty(string collectionPropertyName) => new(this, collectionPropertyName);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a property access in a rule expression.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("\\{{Expression,nq}.{PropertyName,nq}\\}")]
|
||||
public class Property : Expression
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the containing object of the field or property.
|
||||
/// </summary>
|
||||
public Expression? Expression { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the property.
|
||||
/// </summary>
|
||||
public string PropertyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Property"/> class with no containing expression.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">The name of the property.</param>
|
||||
public Property(string propertyName) : this((Expression?)null, propertyName)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Property"/> class with a parameter name.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The name of the parameter.</param>
|
||||
/// <param name="propertyName">The name of the property.</param>
|
||||
public Property(string parameterName, string propertyName) : this(new Parameter(parameterName), propertyName)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Property"/> class.
|
||||
/// </summary>
|
||||
/// <param name="expression">The containing expression.</param>
|
||||
/// <param name="propertyName">The name of the property.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="propertyName"/> is null.</exception>
|
||||
public Property(Expression? expression, string propertyName)
|
||||
{
|
||||
Expression = expression;
|
||||
// maybe do some regex validation for args to ensure it's not a bogus name (no whitespace, no punctuation marks, etc)
|
||||
PropertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitProperty(this);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a string literal value in a rule expression.
|
||||
/// </summary>
|
||||
public class StringLiteral : Literal<string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringLiteral"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The string value.</param>
|
||||
public StringLiteral(string value) : base(value) { }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the types of rule expressions for comparisons and operations.
|
||||
/// </summary>
|
||||
public enum Type
|
||||
{
|
||||
/// <summary>Equality comparison.</summary>
|
||||
Equal,
|
||||
/// <summary>Inequality comparison.</summary>
|
||||
NotEqual,
|
||||
/// <summary>Greater than comparison.</summary>
|
||||
GreaterThan,
|
||||
/// <summary>Greater than or equal comparison.</summary>
|
||||
GreaterThanOrEqual,
|
||||
/// <summary>Less than comparison.</summary>
|
||||
LessThan,
|
||||
/// <summary>Less than or equal comparison.</summary>
|
||||
LessThanOrEqual,
|
||||
|
||||
/// <summary>In operation (value in set).</summary>
|
||||
In,
|
||||
/// <summary>None equal operation.</summary>
|
||||
NoneEqual,
|
||||
/// <summary>Exclude operation.</summary>
|
||||
Exclude,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a WITH operation for sequential rule evaluation.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Left} WITH {Right}")]
|
||||
public class With : Logical
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="With"/> class.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
public With(BoolExpr left, BoolExpr right) : base(left, right) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitWith(this);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Groups;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a rule group where all rules must evaluate to true (logical AND).
|
||||
/// </summary>
|
||||
public class And : Base
|
||||
{
|
||||
/// <summary>
|
||||
/// Merges two BoolExpr expressions using logical AND.
|
||||
/// </summary>
|
||||
/// <param name="left">The left BoolExpr expression.</param>
|
||||
/// <param name="right">The right BoolExpr expression.</param>
|
||||
/// <returns>An AND expression combining both expressions.</returns>
|
||||
protected override BoolExpr Merge(BoolExpr left, BoolExpr right) => new Expression.And(left, right);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="And"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rules">The collection of rules to include in this AND group.</param>
|
||||
public And(IEnumerable<IRule> rules) : base(rules) { }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Groups;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for rule groups that provides common functionality for grouping and merging rules.
|
||||
/// </summary>
|
||||
public abstract class Base : IGroup
|
||||
{
|
||||
/// <summary>
|
||||
/// The internal list of rules in this group.
|
||||
/// </summary>
|
||||
protected readonly List<IRule> RuleList;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of rules in this group.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<IRule> Rules => RuleList;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the merged BoolExpr expression for all rules in this group.
|
||||
/// </summary>
|
||||
public BoolExpr Expression => GetExpressions().Aggregate(Merge);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the expressions from all rules in this group.
|
||||
/// </summary>
|
||||
/// <returns>An enumerable of BoolExpr rule expressions.</returns>
|
||||
protected virtual IEnumerable<BoolExpr> GetExpressions() => RuleList.Select(r => r.Expression);
|
||||
|
||||
/// <summary>
|
||||
/// Merges two BoolExpr expressions according to the group's logic.
|
||||
/// </summary>
|
||||
/// <param name="left">The left BoolExpr expression.</param>
|
||||
/// <param name="right">The right BoolExpr expression.</param>
|
||||
/// <returns>The merged BoolExpr expression.</returns>
|
||||
protected abstract BoolExpr Merge(BoolExpr left, BoolExpr right);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Base"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rules">The collection of rules to include in this group.</param>
|
||||
protected Base(IEnumerable<IRule> rules)
|
||||
{
|
||||
RuleList = rules.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Groups;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a group of rules that can be evaluated together.
|
||||
/// </summary>
|
||||
public interface IGroup : IRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the collection of rules in this group.
|
||||
/// </summary>
|
||||
IReadOnlyCollection<IRule> Rules { get; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Groups;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a rule group where at least one rule must evaluate to true (logical OR).
|
||||
/// </summary>
|
||||
public class Or : Base
|
||||
{
|
||||
/// <summary>
|
||||
/// Merges two BoolExpr expressions using logical OR.
|
||||
/// </summary>
|
||||
/// <param name="left">The left BoolExpr expression.</param>
|
||||
/// <param name="right">The right BoolExpr expression.</param>
|
||||
/// <returns>An OR expression combining both expressions.</returns>
|
||||
protected override BoolExpr Merge(BoolExpr left, BoolExpr right) => new Expression.Or(left, right);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Or"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rules">The collection of rules to include in this OR group.</param>
|
||||
public Or(IEnumerable<IRule> rules) : base(rules) { }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Groups;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a rule group with sequential rule evaluation (WITH semantics).
|
||||
/// </summary>
|
||||
public class With : Base
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the expressions from all rules, potentially with ordering applied.
|
||||
/// </summary>
|
||||
/// <returns>An enumerable of BoolExpr rule expressions.</returns>
|
||||
protected override IEnumerable<BoolExpr> GetExpressions()
|
||||
{
|
||||
// do some ordering here??
|
||||
return base.GetExpressions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges two BoolExpr expressions using WITH semantics.
|
||||
/// </summary>
|
||||
/// <param name="left">The left BoolExpr expression.</param>
|
||||
/// <param name="right">The right BoolExpr expression.</param>
|
||||
/// <returns>A WITH expression combining both expressions.</returns>
|
||||
protected override BoolExpr Merge(BoolExpr left, BoolExpr right) => new Expression.With(left, right);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="With"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rules">The collection of rules to include in this WITH group.</param>
|
||||
public With(IEnumerable<IRule> rules) : base(rules) { }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a rule that contains a BoolExpr expression for evaluation.
|
||||
/// </summary>
|
||||
public interface IRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the BoolExpr expression that defines the rule logic.
|
||||
/// </summary>
|
||||
public BoolExpr Expression { get; }
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using Strata.SqlTools.Rules.Rule.Expression;
|
||||
using Strata.SqlTools.Rules.Rule.Groups;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a complete set of rules with a unique identifier and a root rule group.
|
||||
/// </summary>
|
||||
public class RuleSet : IGroup
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique identifier for this rule set.
|
||||
/// </summary>
|
||||
public Guid RuleSetId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the root rule group containing all rules in this set.
|
||||
/// </summary>
|
||||
public IGroup RootRuleGroup { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the merged BoolExpr expression from the root rule group.
|
||||
/// </summary>
|
||||
public BoolExpr Expression => RootRuleGroup.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of rules from the root rule group.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<IRule> Rules => RootRuleGroup.Rules;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RuleSet"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rootRuleGroup">The root rule group containing all rules.</param>
|
||||
/// <param name="ruleSetId">The unique identifier for this rule set.</param>
|
||||
public RuleSet(IGroup rootRuleGroup, Guid ruleSetId)
|
||||
{
|
||||
RootRuleGroup = rootRuleGroup;
|
||||
RuleSetId = ruleSetId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all single rules from the rule set, recursively traversing all rule groups.
|
||||
/// </summary>
|
||||
/// <returns>An enumerable of all single rules in the rule set.</returns>
|
||||
public IEnumerable<SingleRule> GetAllSingleRules() => GetAllSingleRules(RootRuleGroup);
|
||||
|
||||
/// <summary>
|
||||
/// Recursively gets all single rules from a rule group and its nested groups.
|
||||
/// </summary>
|
||||
/// <param name="group">The rule group to traverse.</param>
|
||||
/// <returns>An enumerable of all single rules found in the group.</returns>
|
||||
private static IEnumerable<SingleRule> GetAllSingleRules(IGroup group)
|
||||
{
|
||||
var rules = new List<SingleRule>();
|
||||
foreach (var rule in group.Rules)
|
||||
{
|
||||
switch (rule)
|
||||
{
|
||||
case Base childGroup:
|
||||
rules.AddRange(GetAllSingleRules(childGroup));
|
||||
break;
|
||||
case SingleRule single:
|
||||
rules.Add(single);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return rules;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single rule with a name and a BoolExpr expression.
|
||||
/// </summary>
|
||||
public class SingleRule : IRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the rule.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the BoolExpr expression that defines the rule logic.
|
||||
/// </summary>
|
||||
public BoolExpr Expression { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property of the input that is used in the <see cref="Expression"/>.
|
||||
/// Returns null if the Expression does not use a property from the input.
|
||||
/// </summary>
|
||||
public Property? Property
|
||||
{
|
||||
get
|
||||
{
|
||||
return Expression switch
|
||||
{
|
||||
Comparison { Left: Property property } => property,
|
||||
Any any => any.CollectionProperty,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SingleRule"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the rule.</param>
|
||||
/// <param name="expression">The BoolExpr expression that defines the rule logic.</param>
|
||||
public SingleRule(string name, BoolExpr expression)
|
||||
{
|
||||
Name = name;
|
||||
Expression = expression;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using System.Dynamic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using RulesEngine.Interfaces;
|
||||
using RulesEngine.Models;
|
||||
using Strata.SqlTools.Rules.Rule;
|
||||
|
||||
[assembly: InternalsVisibleTo("Strata.SqlTools.Rules.Tests", AllInternalsVisible = true)]
|
||||
|
||||
namespace Strata.SqlTools.Rules;
|
||||
|
||||
/// <summary>
|
||||
/// Engine for executing rule sets by translating them to the Microsoft RulesEngine format.
|
||||
/// </summary>
|
||||
internal class RuleSetEngine
|
||||
{
|
||||
private readonly IRulesEngine _innerRulesEngine;
|
||||
private readonly RuleTranslator _translator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RuleSetEngine"/> class with default dependencies.
|
||||
/// </summary>
|
||||
public RuleSetEngine() : this(new RulesEngine.RulesEngine(), new RuleTranslator())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RuleSetEngine"/> class with specified dependencies.
|
||||
/// </summary>
|
||||
/// <param name="innerRulesEngine">The underlying rules engine to use for execution.</param>
|
||||
/// <param name="translator">The translator to convert rule sets to workflow format.</param>
|
||||
internal RuleSetEngine(IRulesEngine innerRulesEngine, RuleTranslator translator)
|
||||
{
|
||||
_innerRulesEngine = innerRulesEngine;
|
||||
_translator = translator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes all rules in the rule set against the provided input.
|
||||
/// </summary>
|
||||
/// <param name="ruleSet">The rule set to execute.</param>
|
||||
/// <param name="input">The input object to evaluate against the rules.</param>
|
||||
/// <returns>A task representing the asynchronous operation, containing true if all rules passed, false otherwise.</returns>
|
||||
public async ValueTask<bool> RunRules(RuleSet ruleSet, object input)
|
||||
{
|
||||
var name = ruleSet.RuleSetId.ToString();
|
||||
|
||||
if (!_innerRulesEngine.ContainsWorkflow(name))
|
||||
{
|
||||
var workflow = _translator.TranslateRuleSet(ruleSet);
|
||||
workflow.WorkflowName = name;
|
||||
_innerRulesEngine.AddOrUpdateWorkflow(workflow);
|
||||
}
|
||||
|
||||
var result = await _innerRulesEngine.ExecuteAllRulesAsync(name, new RuleParameter("input", input));
|
||||
|
||||
var success = result?.TrueForAll(tree => tree.IsSuccess) ?? false;
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an object to an ExpandoObject by copying all public properties.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to convert.</param>
|
||||
/// <returns>An ExpandoObject containing all properties from the source object.</returns>
|
||||
private static ExpandoObject ConvertObjectToExpando(object obj)
|
||||
{
|
||||
var expando = new ExpandoObject();
|
||||
var dictionary = expando as IDictionary<string, object?>;
|
||||
foreach (var property in obj.GetType().GetProperties())
|
||||
{
|
||||
dictionary.Add(property.Name, property.GetValue(obj));
|
||||
}
|
||||
|
||||
return expando;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translates rule expressions from Strata Domain-Specific-Language to Microsoft.RulesEngine format.
|
||||
/// </summary>
|
||||
internal class RuleTranslator
|
||||
{
|
||||
private readonly IVisitor<string> _localRuleVisitor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RuleTranslator"/> class with a default local rule visitor.
|
||||
/// </summary>
|
||||
public RuleTranslator() : this(new LocalVisitor())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RuleTranslator"/> class with a specified rule visitor.
|
||||
/// </summary>
|
||||
/// <param name="localRuleVisitor">The rule visitor to use for translating expressions to strings.</param>
|
||||
public RuleTranslator(IVisitor<string> localRuleVisitor)
|
||||
{
|
||||
_localRuleVisitor = localRuleVisitor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translates a rule set to a Microsoft RulesEngine workflow.
|
||||
/// </summary>
|
||||
/// <param name="ruleSet">The rule set to translate.</param>
|
||||
/// <returns>A workflow containing the translated rules.</returns>
|
||||
public Workflow TranslateRuleSet(RuleSet ruleSet)
|
||||
{
|
||||
var workflowRules = new List<RulesEngine.Models.Rule>();
|
||||
|
||||
var ruleSetRule = TranslateRule(ruleSet);
|
||||
workflowRules.Add(ruleSetRule);
|
||||
|
||||
return new Workflow
|
||||
{
|
||||
RuleExpressionType = RulesEngine.Models.RuleExpressionType.LambdaExpression,
|
||||
Rules = workflowRules
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translates a single rule to a Microsoft RulesEngine rule.
|
||||
/// </summary>
|
||||
/// <param name="rule">The rule to translate.</param>
|
||||
/// <returns>A Microsoft RulesEngine rule with the expression converted to a string.</returns>
|
||||
public RulesEngine.Models.Rule TranslateRule(IRule rule)
|
||||
{
|
||||
var expressionString = rule.Expression.Accept(_localRuleVisitor);
|
||||
return new RulesEngine.Models.Rule
|
||||
{
|
||||
RuleName = "R1",
|
||||
Expression = expressionString
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<!-- NuGet Package Metadata -->
|
||||
<PackageId>Strata.SqlTools.Rules</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Strata Decision Technology</Authors>
|
||||
<Company>Strata Decision Technology</Company>
|
||||
<Product>Strata SQL Utilities - Rules Engine</Product>
|
||||
<Description>Rules engine for Strata.SqlTools, providing rule-based validation and analysis of SQL queries and expressions.</Description>
|
||||
<PackageTags>sql;rules-engine;validation;analysis;query-validation</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/stratadecision/sql-builder</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/stratadecision/sql-builder</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageReleaseNotes>Initial release with rules engine for SQL query validation and analysis.</PackageReleaseNotes>
|
||||
<Copyright>Copyright © Strata Decision Technology 2024-2026</Copyright>
|
||||
|
||||
<!-- Build Configuration -->
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>symbols.nupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
|
||||
|
||||
<!-- Code Analysis -->
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RulesEngine" Version="5.0.3" PrivateAssets="compile;contentfiles;build;analyzers" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user