diff --git a/Directory.Build.props b/Directory.Build.props
index ce33460..7bba322 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -11,7 +11,7 @@
Simpra
ALTA Software llc.
Copyright © 2024 ALTA Software llc.
- 2.0.3
+ 2.1.4
diff --git a/src/AltaSoft.Simpra/InternalLanguageFunctions.cs b/src/AltaSoft.Simpra/InternalLanguageFunctions.cs
index bcdef88..3afa5f0 100644
--- a/src/AltaSoft.Simpra/InternalLanguageFunctions.cs
+++ b/src/AltaSoft.Simpra/InternalLanguageFunctions.cs
@@ -31,10 +31,10 @@ internal static class InternalLanguageFunctions
public static SimpraString @string(SimpraBool input) => new(input.ToString());
public static SimpraString @string(SimpraDate input) => new(input.ToString());
- public static SimpraNumber number(SimpraString input) => new(input.Value);
- public static SimpraNumber number(SimpraBool input) => new(input.Value);
+ public static SimpraNumber number(SimpraString input) => input.HasValue ? new SimpraNumber(input.Value) : (SimpraNumber)SimpraNumber.NoValue;
+ public static SimpraNumber number(SimpraBool input) => input.HasValue ? new SimpraNumber(input.Value) : (SimpraNumber)SimpraNumber.NoValue;
- public static SimpraDate date(SimpraString input) => new(input.Value);
+ public static SimpraDate date(SimpraString input) => input.HasValue ? new SimpraDate(input.Value) : (SimpraDate)SimpraDate.NoValue;
#pragma warning restore IDE1006 // Naming Styles
// ReSharper enable InconsistentNaming
diff --git a/src/AltaSoft.Simpra/Types/SimpraNumber.cs b/src/AltaSoft.Simpra/Types/SimpraNumber.cs
index a71d6d0..a3e1245 100644
--- a/src/AltaSoft.Simpra/Types/SimpraNumber.cs
+++ b/src/AltaSoft.Simpra/Types/SimpraNumber.cs
@@ -40,17 +40,17 @@ private void SetValue(decimal? value)
public static implicit operator decimal(SimpraNumber value) => value.Value;
public static implicit operator bool(SimpraNumber value) => value.Value != 0m;
- public static implicit operator sbyte?(SimpraNumber value) => (sbyte?)value.Value;
- public static implicit operator byte?(SimpraNumber value) => (byte?)value.Value;
- public static implicit operator short?(SimpraNumber value) => (short?)value.Value;
- public static implicit operator ushort?(SimpraNumber value) => (ushort?)value.Value;
- public static implicit operator int?(SimpraNumber value) => (int?)value.Value;
- public static implicit operator uint?(SimpraNumber value) => (uint?)value.Value;
- public static implicit operator long?(SimpraNumber value) => (long?)value.Value;
- public static implicit operator ulong?(SimpraNumber value) => (ulong?)value.Value;
- public static implicit operator float?(SimpraNumber value) => (float?)value.Value;
- public static implicit operator double?(SimpraNumber value) => (double?)value.Value;
- public static implicit operator decimal?(SimpraNumber value) => value.Value;
+ public static implicit operator sbyte?(SimpraNumber value) => value.HasValue ? (sbyte?)value.Value : null;
+ public static implicit operator byte?(SimpraNumber value) => value.HasValue ? (byte?)value.Value : null;
+ public static implicit operator short?(SimpraNumber value) => value.HasValue ? (short?)value.Value : null;
+ public static implicit operator ushort?(SimpraNumber value) => value.HasValue ? (ushort?)value.Value : null;
+ public static implicit operator int?(SimpraNumber value) => value.HasValue ? (int?)value.Value : null;
+ public static implicit operator uint?(SimpraNumber value) => value.HasValue ? (uint?)value.Value : null;
+ public static implicit operator long?(SimpraNumber value) => value.HasValue ? (long?)value.Value : null;
+ public static implicit operator ulong?(SimpraNumber value) => value.HasValue ? (ulong?)value.Value : null;
+ public static implicit operator float?(SimpraNumber value) => value.HasValue ? (float?)value.Value : null;
+ public static implicit operator double?(SimpraNumber value) => value.HasValue ? (double?)value.Value : null;
+ public static implicit operator decimal?(SimpraNumber value) => value.HasValue ? value.Value : null;
public static implicit operator bool?(SimpraNumber value) => value.HasValue ? value.Value != 0m : null;
public static implicit operator SimpraNumber(sbyte value) => new(value);
@@ -113,7 +113,7 @@ public override readonly bool Equals(object? obj)
{
if (obj is null)
return !HasValue;
- if (obj is SimpraDate simpraVar)
+ if (obj is SimpraNumber simpraVar)
return Equals(simpraVar);
return obj.GetType().IsNumber() && Value == Convert.ToDecimal(obj, CultureInfo.InvariantCulture);
diff --git a/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs b/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs
index 433a79b..50b4051 100644
--- a/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs
+++ b/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs
@@ -3,6 +3,7 @@
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
+using AltaSoft.DomainPrimitives;
using AltaSoft.Simpra.Types;
using Antlr4.Runtime.Tree;
@@ -229,9 +230,23 @@ public Expression VisitMemberAccess(SimpraParser.MemberAccessContext context)
var exprProperty2 = Expression.Property(variable, propertyName);
var notNull = Expression.NotEqual(variable, Expression.Constant(null, exprBaseObject.Type));
- var defaultValue = Expression.Default(exprProperty2.Type);
+ // Domain primitives throw when their uninitialized default value is read, so if the property
+ // is a domain primitive, unwrap it to its underlying type before building the null-fallback
+ // default. Otherwise the fallback would be an uninitialized domain primitive that throws as
+ // soon as ConvertToSimpraType tries to convert it.
+ Expression truePart = exprProperty2;
+ Expression defaultValue;
+ if (exprProperty2.Type.TryGetUnderlyingDomainPrimitiveType(out var domainType))
+ {
+ truePart = Expression.Convert(exprProperty2, domainType);
+ defaultValue = Expression.Default(domainType);
+ }
+ else
+ {
+ defaultValue = Expression.Default(exprProperty2.Type);
+ }
- var expr = Expression.Condition(notNull, exprProperty2, defaultValue);
+ var expr = Expression.Condition(notNull, truePart, defaultValue);
var block = Expression.Block([variable], assignExpr, expr);
diff --git a/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.tools.cs b/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.tools.cs
index 5b336d9..2133c29 100644
--- a/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.tools.cs
+++ b/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.tools.cs
@@ -205,12 +205,13 @@ private Expression CallFunction(string methodName, Expression[] arguments, Parse
if (method is null)
throw new SimpraException(context, $"Function '{methodName}({string.Join(',', paramTypes.Select(x => x.Name))})' not found");
+ var methodParameters = method.GetParameters();
var convertedArguments = new Expression[paramTypes.Length + (matchWithToken ? 1 : 0)];
for (var i = 0; i < paramTypes.Length; i++)
{
var expr = arguments[i];
- convertedArguments[i] = ConvertToType(expr, paramTypes[i], context);
+ convertedArguments[i] = ConvertToType(expr, methodParameters[i].ParameterType, context);
}
if (matchWithToken)
diff --git a/tests/AltaSoft.Simpra.tests/AsyncExecutionTests.cs b/tests/AltaSoft.Simpra.tests/AsyncExecutionTests.cs
new file mode 100644
index 0000000..4b956fa
--- /dev/null
+++ b/tests/AltaSoft.Simpra.tests/AsyncExecutionTests.cs
@@ -0,0 +1,94 @@
+using AltaSoft.Simpra.Tests.Models;
+using static AltaSoft.Simpra.Tests.Models.TestModelFactory;
+
+namespace AltaSoft.Simpra.Tests;
+
+public class AsyncExecutionTests
+{
+ [Fact]
+ public async Task ExecuteExpression_Should_ReturnTrue_When_CcyIsInListOfCurrencyCodes()
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ const string expressionCode = "return Ccy in (ListOfCurrencyCodes('VisaB2B') + 'USD')";
+ var result = await simpra.ExecuteAsync(model, new TestFunctions(), expressionCode, null, CancellationToken.None);
+ Assert.True(result);
+ }
+
+ [Fact]
+ public async Task Expression_Should_EvaluateSanctionedCountriesExpression_Correctly()
+ {
+ const string expressionCode =
+ """
+ let russianCountries = BigList('sanctioned_countries')
+ let amount = Transfer.Amount
+ let ccy = Transfer.Currency
+
+ let isValidCcy = (ccy is '' or ccy not in ['USD', 'EUR']) and (ccy like 'I%' or ccy matches '[a-zA-Z_][a-zA-Z_0-9]')
+ let isValidAmount = amount > 1000 and amount < 2000
+ let isValidAmount2 = amount > 1000 and < 2000
+ let isValidRemittance = length(Remittance) > 4
+
+ return isValidCcy and isValidAmount and isValidRemittance
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel(); // Model is irrelevant here
+ var result = await simpra.ExecuteAsync(model, new TestFunctions(), expressionCode,
+ new SimpraCompilerOptions { MutabilityOption = MutabilityOption.Immutable, StringComparisonOption = StringComparisonOption.IgnoreCase },
+ CancellationToken.None);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public async Task ExecuteExpression_Should_EvaluateAsyncCallDirectlyWithinArithmeticBinary()
+ {
+ const string expressionCode = "return Compute(3, 4) + 1";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = await simpra.ExecuteAsync(model, new TestFunctions(), expressionCode, null, CancellationToken.None);
+ Assert.Equal(8m, result);
+ }
+
+ [Theory]
+ [InlineData("Compute(1, 1) > 1 and Compute(3, 3) > 5", true)] // left true -> right (async) must be evaluated
+ [InlineData("Compute(1, 1) > 5 and Compute(3, 3) > 5", false)] // left false -> AND short-circuits, right (async) never evaluated
+ [InlineData("Compute(1, 1) > 5 or Compute(3, 3) > 5", true)] // left false -> OR must evaluate right (async)
+ [InlineData("Compute(1, 1) > 1 or Compute(3, 3) > 100", true)] // left true -> OR short-circuits, right (async) never evaluated
+ public async Task ExecuteExpression_Should_ShortCircuitCorrectly_When_AsyncCallIsOnRightOfAndOr(string condition, bool expected)
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = await simpra.ExecuteAsync(model, new TestFunctions(), $"return {condition}", null, CancellationToken.None);
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public async Task ExecuteExpression_Should_EvaluateAsyncCallWithinConditional()
+ {
+ const string expressionCode =
+ "return when Compute(1, 1) > 1 then 'yes' else 'no' end";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = await simpra.ExecuteAsync(model, new TestFunctions(), expressionCode, null, CancellationToken.None);
+ Assert.Equal("yes", result);
+ }
+
+ [Fact]
+ public async Task ExecuteExpression_Should_EvaluateMultipleAsyncCallsCombinedInSingleBinaryExpression()
+ {
+ const string expressionCode = "return Compute(1, 2) + Compute(3, 4)";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = await simpra.ExecuteAsync(model, new TestFunctions(), expressionCode, null, CancellationToken.None);
+ Assert.Equal(10m, result);
+ }
+}
diff --git a/tests/AltaSoft.Simpra.tests/BuiltInFunctionsTests.cs b/tests/AltaSoft.Simpra.tests/BuiltInFunctionsTests.cs
new file mode 100644
index 0000000..0ce9963
--- /dev/null
+++ b/tests/AltaSoft.Simpra.tests/BuiltInFunctionsTests.cs
@@ -0,0 +1,315 @@
+using AltaSoft.Simpra.Tests.Models;
+using static AltaSoft.Simpra.Tests.Models.TestModelFactory;
+
+namespace AltaSoft.Simpra.Tests;
+
+public class BuiltInFunctionsTests
+{
+ [Theory]
+ // basic / within bounds
+ [InlineData("USD", 1, 3, "USD")] // from U
+ [InlineData("USD", 1, 2, "US")]
+ [InlineData("USD", 2, 2, "SD")] // from S
+ [InlineData("USD", 3, 1, "D")] // from D
+
+ // length clamping at the end
+ [InlineData("USD", 1, 10003, "USD")]
+ [InlineData("USD", 3, 10, "D")]
+
+ // start beyond end
+ [InlineData("USD", 1000, 1, "")]
+ [InlineData("USD", 4, 1, "")] // beyond "USD"
+
+ // start exactly at end
+ [InlineData("USD", 4, 0, "")]
+ [InlineData("USD", 4, 10, "")]
+
+ // zero length
+ [InlineData("USD", 1, 0, "")]
+ [InlineData("USD", 2, 0, "")]
+ [InlineData("USD", 3, 0, "")]
+
+ // negative/zero start ? clamp to 1
+ [InlineData("USD", 0, 1, "U")]
+ [InlineData("USD", -5, 2, "US")]
+ [InlineData("USD", -2, 100, "USD")]
+
+ // negative length ? empty
+ [InlineData("USD", 1, -1, "")]
+ [InlineData("USD", 3, -10, "")]
+ [InlineData("USD", -2, -10, "")]
+
+ // empty input
+ [InlineData("", 1, 5, "")]
+ [InlineData("", 10, 1, "")]
+ [InlineData("", -3, 2, "")]
+ [InlineData("", 1, 0, "")]
+
+ // whitespace
+ [InlineData(" ", 1, 1, " ")]
+ [InlineData(" ", 2, 2, " ")]
+ [InlineData(" ", 2, 100, " ")]
+
+ // non-ASCII (safe checks with multi-byte chars)
+ [InlineData("\u0410\u0411\u0412\u0413\u0414\u0415\u0416", 1, 2, "\u0410\u0411")]
+ [InlineData("\u0410\u0411\u0412\u0413\u0414\u0415\u0416", 3, 3, "\u0412\u0413\u0414")]
+ [InlineData("\u0410\u0411\u0412\u0413\u0414\u0415\u0416", 11, 5, "")]
+ [InlineData("\u0410\u0411\u0412\u0413\u0414\u0415\u0416", -3, 100, "\u0410\u0411\u0412\u0413\u0414\u0415\u0416")]
+
+ public void BuiltInFunction_Substring_EdgeCases(string input, int start, int length, string expected)
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Ccy = input;
+ var expressionCode = $"return substring(Ccy,{start},{length})";
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void BuiltInFunction_Substring_ShouldMatchExamplesFromPrompt()
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Ccy = "USD";
+
+ var result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,0,3)");
+ Assert.Equal("USD", result);
+
+ result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,0,10003)");
+ Assert.Equal("USD", result);
+
+ result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,1000,1)");
+ Assert.Equal("", result);
+
+ result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,1000,0)");
+ Assert.Equal("", result);
+
+ result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,1,0)");
+ Assert.Equal("", result);
+ }
+ [Theory]
+ // basic in-bounds
+ [InlineData("USD", 1, "U")]
+ [InlineData("USD", 2, "S")]
+ [InlineData("USD", 3, "D")]
+
+ // at/after end => empty
+ [InlineData("USD", 4, "")]
+ [InlineData("USD", 1000, "")]
+ [InlineData("U", 2, "")]
+ [InlineData("", 1, "")]
+ [InlineData("", 5, "")]
+
+ // zero/negative start ? clamp to 1
+ [InlineData("USD", 0, "U")]
+ [InlineData("USD", -1, "U")]
+ [InlineData("USD", -5, "U")]
+ [InlineData("", -3, "")]
+
+ // whitespace
+ [InlineData(" X ", 1, " ")]
+ [InlineData(" X ", 2, "X")]
+ [InlineData(" X ", 3, " ")]
+
+ // non-ASCII (single UTF-16 code units)
+ [InlineData("???????", 1, "?")]
+ [InlineData("???????", 3, "?")]
+ [InlineData("???????", 6, "?")]
+ [InlineData("???????", 7, "?")]
+ [InlineData("???????", 8, "")]
+
+ // very large index
+ [InlineData("USD", int.MaxValue, "")]
+ public void BuiltInFunction_Substring_StartOnly_EdgeCases(string input, int start, string expected)
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Ccy = input;
+ var expressionCode = $"return substring(Ccy,{start})";
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void BuiltInFunction_Substring_StartOnly_NullSource_ShouldBeEmpty()
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Ccy = null;
+
+ var result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,1)");
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void BuiltInFunction_Substring_NullSource_ShouldBeNull()
+ {
+ // If your DSL defines a behavior for nulls, keep this.
+ // If it should throw instead, change to Assert.Throws.
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Ccy = null!;
+
+ var result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,0,3)");
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void BuiltInFunction_Substring_ShouldReturnCorrectSubstringOfLength1()
+ {
+ const string expressionCode = "return substring(Ccy,1)";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Ccy = "USD";
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal("U", result);
+ }
+
+ [Fact]
+ public void BuiltInFunction_Round_WithDecimalsArgument_ShouldRoundToGivenPrecision()
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), "return round(3.14159, 2)");
+ Assert.Equal(3.14m, result);
+
+ result = simpra.Execute(model, new TestFunctions(), "return round(3.14159, 3)");
+ Assert.Equal(3.142m, result);
+
+ result = simpra.Execute(model, new TestFunctions(), "return round(-3.14159, 2)");
+ Assert.Equal(-3.14m, result);
+ }
+
+ [Fact]
+ public void BuiltInFunction_Round_AtMidpoint_ShouldRoundToNearestEvenInteger()
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), "return round(2.5)");
+ Assert.Equal(2m, result);
+
+ result = simpra.Execute(model, new TestFunctions(), "return round(3.5)");
+ Assert.Equal(4m, result);
+ }
+
+ [Theory]
+ [InlineData(-5, 5)]
+ [InlineData(5, 5)]
+ [InlineData(0, 0)]
+ public void BuiltInFunction_Abs_ShouldReturnAbsoluteValue(int input, int expected)
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), $"return abs({input})");
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void BuiltInFunction_Abs_WithFractionalValue_ShouldReturnAbsoluteValue()
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), "return abs(-3.5)");
+ Assert.Equal(3.5m, result);
+ }
+
+ [Fact]
+ public void BuiltInFunction_String_FromNumber_ShouldReturnDecimalStringRepresentation()
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), "return string(42.5)");
+ Assert.Equal("42.5", result);
+ }
+
+ [Fact]
+ public void BuiltInFunction_String_FromBool_ShouldReturnCapitalizedBooleanText()
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), "return string(true)");
+ Assert.Equal("True", result);
+ }
+
+ [Fact]
+ public void BuiltInFunction_Number_FromString_ShouldParseDecimalValue()
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), "return number('123.45')");
+ Assert.Equal(123.45m, result);
+ }
+
+ [Fact]
+ public void BuiltInFunction_Number_FromBool_ShouldReturnOneForTrueAndZeroForFalse()
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), "return number(true)");
+ Assert.Equal(1m, result);
+
+ result = simpra.Execute(model, new TestFunctions(), "return number(false)");
+ Assert.Equal(0m, result);
+ }
+
+ [Fact]
+ public void BuiltInFunction_Number_FromNullString_ShouldReturnNullInsteadOfThrowing()
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Ccy = null;
+
+ var result = simpra.Execute(model, new TestFunctions(), "return number(Ccy)");
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void BuiltInFunction_Date_FromString_ShouldParseCorrectDate()
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), "return date('2024-03-15')");
+ Assert.Equal(new DateTime(2024, 3, 15), result);
+ }
+
+ [Fact]
+ public void BuiltInFunction_Length_OfBooleanListLiteral_ShouldReturnElementCount()
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), "return length([true, false, true])");
+ Assert.Equal(3m, result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_XIsNotFive_And_LengthIsTwo()
+ {
+ const string expressionCode =
+ """
+ let X = '50'
+ return X is not '5'
+ and length(X) is 2;
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel(); // Model is irrelevant here
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+}
diff --git a/tests/AltaSoft.Simpra.tests/ComplexObjectAccessTests.cs b/tests/AltaSoft.Simpra.tests/ComplexObjectAccessTests.cs
new file mode 100644
index 0000000..65d748d
--- /dev/null
+++ b/tests/AltaSoft.Simpra.tests/ComplexObjectAccessTests.cs
@@ -0,0 +1,490 @@
+using AltaSoft.Simpra.Tests.Models;
+using static AltaSoft.Simpra.Tests.Models.TestModelFactory;
+
+namespace AltaSoft.Simpra.Tests;
+
+public class ComplexObjectAccessTests
+{
+ [Fact]
+ public void DictionaryIndexer_MissingKey_ReturnsDefaultValueForProperty()
+ {
+ const string expressionCode =
+ "return DictionaryOfObjects['test'].Id";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.DictionaryOfObjects = new Dictionary { { "Georgia", new Customer { Id = 1, Status = 10 } } };
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal(0, result);
+ }
+
+ [Fact]
+ public void CallGetValueFromDictionaryWhenKeyDoesNotExist_ShouldReturnDefault()
+ {
+ const string expressionCode =
+ "return Countries['test'] is 'Test'";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Countries = new Dictionary { { "Georgia", "Test" } };
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void CallGetValueFromDictionaryWhenKeyExist_ShouldReturnValue()
+ {
+ const string expressionCode =
+ "return Countries['Georgia'] is 'Test'";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Countries = new Dictionary { { "Georgia", "Test" } };
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ModelWithInheritedClassProperties_ShouldFindPropertyCorrectly()
+ {
+ const string expressionCode =
+ "return Color";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Color = Color.Blue;
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal(Color.Blue, result);
+ }
+
+ [Fact]
+ public void ModelWithInheritedInterfaceProperties_ShouldFindPropertyCorrectly()
+ {
+ const string expressionCode =
+ "return Customer.Id";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal(1, result);
+ }
+
+ [Fact]
+ public void Expression_ReturnNullableEnum_ReturnValueMustBeCorrect()
+ {
+ const string expressionCode =
+ "return NullableEnum";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.NullableEnum = Color.Green;
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.NotNull(result);
+
+ model.NullableEnum = null;
+ result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void Expression_ShouldCompareNullableEnum_ReturnValueMustBeCorrect()
+ {
+ const string expressionCode =
+ "return NullableEnum is 'Green'";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.NullableEnum = Color.Green;
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_NestedDomainPrimitiveType_ShouldReturnCorrectly()
+ {
+ const string expressionCode =
+ "return Transfer.RegulatoryReporting[1].Details[1].Information[1]";
+
+ var simpra = new Simpra();
+ var model = Iso20022TransferModel.CreateForInformation();
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.NotNull(result);
+ }
+
+ [Fact]
+ public void Expression_NestedDomainPrimitiveType_ShouldCompareCorrectly()
+ {
+ const string expressionCode =
+ "return Transfer.RegulatoryReporting[1].Details[1].Information[1] is 'Information1'";
+
+ var simpra = new Simpra();
+ var model = Iso20022TransferModel.CreateForInformation();
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ExpressionListOfList_Comparison_ShouldReturnCorrectly()
+ {
+ const string expressionCode =
+ "return ListOfList[2][2] is 4";
+
+ var simpra = new Simpra();
+ var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"], ListOfList = [[1, 2], [3, 4]] };
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ListOfList_ShouldReturnCorrectly()
+ {
+ const string expressionCode =
+ "return ListOfList";
+
+ var simpra = new Simpra();
+ var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"], ListOfList = [[1, 2], [3, 4]] };
+
+ var result = simpra.Execute>, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal(4, result[1][1]);
+ }
+
+ [Fact]
+ public void GetComplexObject_ShouldReturnCorrectly()
+ {
+ const string expressionCode = "return Customer";
+
+ var simpra = new Simpra();
+ var model = GetTestModel(); // Model is irrelevant here
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal(1, result.Id);
+ }
+
+ [Fact]
+ public void EnumerableOfIntegers_ShouldReturnCorrectly()
+ {
+ const string expressionCode =
+ "return IntegerEnumerable";
+
+ var simpra = new Simpra();
+ var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"], IntegerEnumerable = [1, 2, 3] };
+
+ var result = simpra.Execute, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal(2, result.ToList()[1]);
+ }
+
+ [Fact]
+ public void ArrayOfIntegers_ShouldReturnCorrectly()
+ {
+ const string expressionCode =
+ "return IntegerArray";
+
+ var simpra = new Simpra();
+ var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"], IntegerArray = [1, 2, 3] };
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal(2, result[1]);
+ }
+
+ [Fact]
+ public void ListOfComplexObjects_ShouldReturnCorrectly()
+ {
+ const string expressionCode =
+ "return ComplexList";
+
+ var simpra = new Simpra();
+ var model = new ListModel
+ {
+ EnumList = [Color.Blue, Color.Green],
+ IntegerList = [1, 2, 3],
+ StringList = ["test", "test2"],
+ ComplexList =
+ [
+ new Customer { Id = 1, Status = 1 },
+ new Customer { Id = 2, Status = 2 }
+ ]
+ };
+
+ var result = simpra.Execute, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal(2, result[1].Id);
+ }
+
+ [Fact]
+ public void ExpressionStringListEqualsValue_ShouldReturnCorrectValues()
+ {
+ const string expressionCode =
+ "return StringList[1] is 'test'";
+
+ var simpra = new Simpra();
+ var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"] };
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void StringList_ShouldReturnCorrectValues()
+ {
+ const string expressionCode =
+ "return StringList";
+
+ var simpra = new Simpra();
+ var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"] };
+
+ var result = simpra.Execute, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal("test2", result[1]);
+ }
+
+ [Fact]
+ public void EnumList_ShouldReturnCorrectValues()
+ {
+ const string expressionCode =
+ "return EnumList";
+
+ var simpra = new Simpra();
+ var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"] };
+
+ var result = simpra.Execute, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal(Color.Green, result[1]);
+ }
+
+ [Fact]
+ public void IntegerList_ShouldReturnCorrectValues()
+ {
+ const string expressionCode =
+ "return IntegerList";
+
+ var simpra = new Simpra();
+ var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"] };
+
+ var result = simpra.Execute, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode);
+ Assert.Equal(2, result[1]);
+ }
+
+ [Fact]
+ public void Execute_ShouldReturnListOfComplexObject()
+ {
+ const string expressionCode =
+ "return Transfer.RegulatoryReporting";
+
+ var simpra = new Simpra();
+ var model = Iso20022TransferModel.CreateForCountry("FR");
+
+ var result = simpra.Execute, Iso20022TransferModel, TestFunctions>(model, new TestFunctions(), expressionCode);
+ Assert.Equal("FR", result[0].Authority.Country);
+ }
+
+ [Fact]
+ public void Execute_ShouldReturnComplexObject_WhenAccessedViaIndex()
+ {
+ const string expressionCode =
+ "return Transfer.RegulatoryReporting[1]";
+
+ var simpra = new Simpra();
+ var model = Iso20022TransferModel.CreateForCountry("FR");
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal("FR", result.Authority.Country);
+ }
+
+ [Fact]
+ public void Execute_ShouldReturnTrue_WhenAuthorityCountryIsFR()
+ {
+ const string expressionCode =
+ "return Transfer.RegulatoryReporting[1].Authority.Country is 'FR'";
+
+ var simpra = new Simpra();
+ var model = Iso20022TransferModel.CreateForCountry("FR");
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnFalse_When_IndexIsOutOfRangeAndValueCompared()
+ {
+ const string expressionCode =
+ "return Transfer.A[10] is 1";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer!.A = [1, 2, 3];
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnFalse_When_NestedListIsUsed()
+ {
+ const string expressionCode =
+ "return Transfer.OuterList[10].InnerList[1] is 1";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer!.A = [1, 2, 3];
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnFalse_When_NestedListIsUsedX()
+ {
+ const string expressionCode =
+ "return CustomerList[1] has value";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.CustomerList = new List();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnFalse_When_ArrayIsNullAndValueCompared()
+ {
+ const string expressionCode =
+ "return Transfer.A[1] is 1";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer = null;
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnDefault_When_ArrayIsNull()
+ {
+ const string expressionCode =
+ "return Transfer.A[10]";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer = null;
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal(0, result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnFalse_When_TheValueIsNull()
+ {
+ const string expressionCode =
+ "return Transfer.Customer.Id is 1";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer = null;
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ExecuteExpression_Should_ReturnFalse_When_Nint1HasNoValue()
+ {
+ const string expressionCode =
+ """
+ let X = CustomerId
+ let Y = Nint1
+ return Y has value
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ExecuteExpression_WithNullCheck_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new TestModelMain { Test = null };
+ const string expression = "return Test has value";
+
+ var result = simpra.Execute(model, new TestFunctions(), expression);
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ExecuteExpression_Should_ReturnTrue_When_PropertyIsEnum()
+ {
+ const string expression = " return Test.Test is 'Test1';";
+ var simpra = new Simpra();
+ var model = new TestModelMain { Test = new TestModel1 { Test = TestModel2.Test1 } };
+ var result = simpra.Execute(model, new TestFunctions(), expression);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ExecuteWitNullableEnum_ShouldReturnValue()
+ {
+ const string expressionCode = "return Nint1";
+
+ var simpra = new Simpra();
+ var model = GetTestModel(); // Model is irrelevant here
+ model.Nint1 = 1;
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal(1, result);
+ }
+
+ [Fact]
+ public void Execute_WithValidColorReference_ShouldReturnGreen()
+ {
+ const string expressionCode = "return Color";
+
+ var simpra = new Simpra();
+ var model = GetTestModel(); // Model is irrelevant here
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal("Green", result);
+ }
+
+ [Fact]
+ public void Execute_WithUnknownColorReference_ShouldReturnGreen()
+ {
+ const string expressionCode = "return ColorX";
+
+ var simpra = new Simpra();
+ var model = GetTestModel(); // Model is irrelevant here
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal("Green", result);
+ }
+
+ [Fact]
+ public void ExecuteSyntax_WithArithmeticOperations_ShouldReturnCorrectValue()
+ {
+ const string expressionCode = "return ColorM";
+
+ var simpra = new Simpra();
+ var model = GetTestModel(); // Model is irrelevant here
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal("Green", result);
+ }
+}
diff --git a/tests/AltaSoft.Simpra.tests/CompositeExpressionTests.cs b/tests/AltaSoft.Simpra.tests/CompositeExpressionTests.cs
new file mode 100644
index 0000000..a546642
--- /dev/null
+++ b/tests/AltaSoft.Simpra.tests/CompositeExpressionTests.cs
@@ -0,0 +1,263 @@
+using AltaSoft.Simpra.Tests.Models;
+using static AltaSoft.Simpra.Tests.Models.TestModelFactory;
+
+namespace AltaSoft.Simpra.Tests;
+
+public class CompositeExpressionTests
+{
+ [Fact]
+ public void Expression_Should_ReturnMultipleValues_When_AggregateListValues()
+ {
+ const string expressionCode =
+ """
+ let values = [1, 2, 3, 4, 5]
+ let sum = sum(values)
+ let avg = sum(values) / length(values)
+ return sum + avg
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal(18m, result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_AmountIsGreaterThanMaxOfList()
+ {
+ const string expressionCode =
+ """
+ let amounts = [10, 20, 30, 40]
+ let maxAmount = amounts[3]
+ return Transfer.Amount > maxAmount
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer!.Amount = 50;
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_CheckingForNullValue()
+ {
+ const string expressionCode =
+ """
+ let transfer = Transfer
+ return transfer.Currency has value
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer!.Currency = "USD";
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_AmountInCurrencyIsGreaterThanThresholdAndMatchesPattern()
+ {
+ const string expressionCode =
+ """
+ let transfer = Transfer
+ let amount = transfer.Amount
+ let ccy = transfer.Currency
+ return amount > 100 and (ccy matches '^[A-Z]{3}$') and ccy is 'USD'
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer!.Amount = 150;
+ model.Transfer.Currency = "USD";
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnFalse_When_DividingByZeroHandledProperly()
+ {
+ const string expressionCode =
+ """
+ let transfer = Transfer
+ let amount = transfer.Amount
+ let divisor = 0
+ let safeDivision = when divisor is not 0 then amount / divisor else 0 end
+ return safeDivision is 0
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_DynamicStringListContainsMatchingItem()
+ {
+ const string expressionCode =
+ """
+ let dynamicList = ListSomeCountries('countries')
+ return 'RU' in dynamicList or 'US' in dynamicList
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_DynamicIntListContainsMatchingItem()
+ {
+ const string expressionCode =
+ """
+ let dynamicList = ListOfCustomerIds('Good')
+ return 1 in dynamicList
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnFalse_When_AmountIsNegativeAndCurrencyIsEUR()
+ {
+ const string expressionCode =
+ """
+ let transfer = Transfer
+ let amount = transfer.Amount
+ let ccy = transfer.Currency
+ return amount < 0 and ccy is 'EUR'
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer!.Amount = -50;
+ model.Transfer.Currency = "EUR";
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_SomeComplexConditionIsMet()
+ {
+ const string expressionCode =
+ """
+ let x = 100
+ let y = 'USD'
+ let transfer = Transfer
+ let amount = transfer.Amount
+ return amount < 500 and y is 'USD'
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer!.Amount = 100;
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnComplexCalculation_When_CombinedWithArithmetic()
+ {
+ const string expressionCode =
+ """
+ let amount = Transfer.Amount
+ let fee = amount * 0.05
+ let totalAmount = amount - fee
+ return totalAmount
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer!.Amount = 200;
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal(190, result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_AmountIsWithinRange_And_CurrencyIsValid_And_AmountIncreasedByCustomLogic()
+ {
+ const string expressionCode =
+ """
+ let transfer = Transfer
+ let amount = transfer.Amount
+ let currency = transfer.Currency
+ let threshold = 500
+ let validCurrencies = ['USD', 'EUR', 'GBP']
+ let increaseAmountByPercentage = amount + (amount * (5 / 100))
+ let increasedAmount = when currency in validCurrencies then increaseAmountByPercentage else amount end
+ return increasedAmount > 180 and increasedAmount < threshold
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer!.Amount = 180;
+ model.Transfer.Currency = "USD";
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnCorrectResult_When_NestedListOperationsAndStringManipulationsAreUsed()
+ {
+ const string expressionCode =
+ """
+ let items = ['USD', 'EUR', 'LIRA']
+ let priceList = [2.8, 3.0, 0.75]
+ let targetItem = 'USD'
+ let targetPrice = when targetItem in items then 3 else 2 end
+ let discountedPrice = when targetPrice > 1.0 then targetPrice * 0.9 else targetPrice end
+ let result = targetItem + ' costs ' + discountedPrice
+ return result
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal("USD costs 2.7", result);
+ }
+
+ [Fact]
+ public void ExecuteExpression_Should_ReturnSumOfXAndY_When_XyPropertiesAreUsed()
+ {
+ const string expressionCode =
+ """
+ let X = Xy.X
+ let Y = Xy.Y
+ return X + Y
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal(3, result);
+ }
+}
diff --git a/tests/AltaSoft.Simpra.tests/ControlFlowTests.cs b/tests/AltaSoft.Simpra.tests/ControlFlowTests.cs
new file mode 100644
index 0000000..65b1388
--- /dev/null
+++ b/tests/AltaSoft.Simpra.tests/ControlFlowTests.cs
@@ -0,0 +1,224 @@
+using AltaSoft.Simpra.Tests.Models;
+using static AltaSoft.Simpra.Tests.Models.TestModelFactory;
+
+namespace AltaSoft.Simpra.Tests;
+
+public class ControlFlowTests
+{
+ [Fact]
+ public void Expression_Should_EvaluateNestedWhenConditionAndReturnCorrectResult()
+ {
+ const string expressionCode =
+ """
+ let amount = Transfer.Amount
+ let result = when amount < 50 then 'Low'
+ when amount >= 50 and amount < 200 then 'Medium'
+ else 'High'
+ return result
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer!.Amount = 150;
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal("Medium", result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnEmptyString_When_StringContainsNoMatch()
+ {
+ const string expressionCode =
+ """
+ let str = 'foobar'
+ return when str like 'f%' then str else ''
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal("foobar", result);
+ }
+
+ [Fact]
+ public void Expression_Should_HandleLargeNestedCondition()
+ {
+ const string expressionCode =
+ """
+ let x = Transfer.Amount
+ let y = Transfer.Currency
+ return when x < 50 then 'Low'
+ when x >= 50 and x <= 150 then 'Medium'
+ when x > 150 and y is 'USD' then 'High - USD'
+ when x > 150 and y is 'EUR' then 'High - EUR'
+ else 'Unknown'
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer!.Amount = 200;
+ model.Transfer.Currency = "USD";
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal("High - USD", result);
+ }
+
+ [Fact]
+ public void ExecuteExpression_Should_ReturnCorrectValue_FromNestedIfWithElse()
+ {
+ const string expressionCode =
+ """
+ let x = Transfer.Amount
+ if x > 7 then
+ return 10
+ else if x > 5 then
+ return 6
+ else if x > 1 then
+ return 2
+ else
+ return round(1000)
+ end
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ model.Transfer!.Amount = 8;
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal(10, result);
+
+ model.Transfer!.Amount = 6;
+ result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal(6, result);
+
+ model.Transfer!.Amount = 2;
+ result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal(2, result);
+
+ model.Transfer!.Amount = -1;
+ result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal(1000, result);
+ }
+
+ [Fact]
+ public void ExecuteExpression_Should_ReturnCorrectValue_FromNestedIfWithoutElse()
+ {
+ const string expressionCode =
+ """
+ let x = Transfer.Amount
+ if x > 7 then
+ return 10
+ if x > 5 then
+ return 6
+ if x > 1 then
+ return 2
+ else
+ return 1000
+ end
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer!.Amount = 8;
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal(10, result);
+
+ model.Transfer!.Amount = 6;
+ result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal(6, result);
+
+ model.Transfer!.Amount = 2;
+ result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal(2, result);
+
+ model.Transfer!.Amount = -1;
+ result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal(1000, result);
+ }
+
+ [Fact]
+ public void ExecuteExpression_Should_ReturnFirstReturnValue_When_MultipleReturnStatements()
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ const string expressionCode =
+ """
+ return 10
+ return 20
+ """;
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal(10, result);
+ }
+
+ [Fact]
+ public void ExecuteExpression_WithMultipleReturnStatements_ShouldReturnLastValue()
+ {
+ var simpra = new Simpra();
+ var model = new TestModelMain { Test = null };
+ const string expression = """
+ return 10
+ return true
+ """;
+
+ var result = simpra.Execute(model, new TestFunctions(), expression);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTwo_When_XIsSixty()
+ {
+ const string expressionCode =
+ """
+ let X = 50 + 10.0
+ let Y = when X > 100 then 1 when X > 10 then 2 else 0
+ return Y
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel(); // Model is irrelevant here
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal(2, result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnThree_When_bIsThirty()
+ {
+ const string expressionCode =
+ """
+ let b = 30;
+ let x = 1;
+ return when b is 1 then 1 else 3 end
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel(); // Model is irrelevant here
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal(3, result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnOne_When_XIsNotFive()
+ {
+ const string expressionCode =
+ """
+ let X = '50'
+ return when X is not '5' then 1 else 0 end
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel(); // Model is irrelevant here
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal(1, result);
+ }
+}
diff --git a/tests/AltaSoft.Simpra.tests/ErrorHandlingTests.cs b/tests/AltaSoft.Simpra.tests/ErrorHandlingTests.cs
new file mode 100644
index 0000000..c39a448
--- /dev/null
+++ b/tests/AltaSoft.Simpra.tests/ErrorHandlingTests.cs
@@ -0,0 +1,102 @@
+using AltaSoft.Simpra.Tests.Models;
+using static AltaSoft.Simpra.Tests.Models.TestModelFactory;
+
+namespace AltaSoft.Simpra.Tests;
+
+public class ErrorHandlingTests
+{
+ [Fact]
+ public void InvalidSimpraSyntax_ShouldThrowException_WhenIncorrectAndSignIsUsedAndReturnStatement()
+ {
+ const string expressionCode =
+ " return Amount is 100 && Amount is 200";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ // ReSharper disable ConvertToLocalFunction
+ var f = () => simpra.Execute(model, new TestFunctions(), expressionCode);
+ // ReSharper restore ConvertToLocalFunction
+ Assert.Throws(() => f());
+ }
+
+ [Fact]
+ public void InvalidSimpraSyntax_ShouldThrowException_WhenIncorrectAndSignIsUsed()
+ {
+ const string expressionCode =
+ " Amount is 100 && Amount is 200";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ // ReSharper disable ConvertToLocalFunction
+ var f = () => simpra.Execute(model, new TestFunctions(), expressionCode);
+ // ReSharper restore ConvertToLocalFunction
+ Assert.Throws(() => f());
+ }
+
+ [Fact]
+ public void InvalidSimpraSyntax_ShouldThrowException_WithoutReturn()
+ {
+ const string expressionCode =
+ " 111 Amount is 100";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ // ReSharper disable ConvertToLocalFunction
+ var f = () => simpra.Execute(model, new TestFunctions(), expressionCode);
+ // ReSharper restore ConvertToLocalFunction
+ Assert.Throws(() => f());
+ }
+
+ [Fact]
+ public void InvalidSimpraSyntax_ShouldThrowException_WithReturn()
+ {
+ const string expressionCode =
+ "return 111 Amount is 100";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ // ReSharper disable ConvertToLocalFunction
+ var f = () => simpra.Execute(model, new TestFunctions(), expressionCode);
+ // ReSharper restore ConvertToLocalFunction
+ Assert.Throws(() => f());
+ }
+
+ [Fact]
+ public void Division_ByZero_ShouldThrowDivideByZeroException()
+ {
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var f = () => simpra.Execute(model, new TestFunctions(), "return 5 / 0");
+
+ Assert.Throws(() => f());
+ }
+
+ [Fact]
+ public void CallingUndefinedFunction_ShouldThrowSimpraException()
+ {
+ const string expressionCode = "return NonExistentFunction()";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var f = () => simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Throws(() => f());
+ }
+
+ [Fact]
+ public void AccessingUndefinedProperty_ShouldThrowSimpraException()
+ {
+ const string expressionCode = "return NonExistentProperty";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var f = () => simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Throws(() => f());
+ }
+}
diff --git a/tests/AltaSoft.Simpra.tests/FunctionCallTests.cs b/tests/AltaSoft.Simpra.tests/FunctionCallTests.cs
new file mode 100644
index 0000000..5fbbd76
--- /dev/null
+++ b/tests/AltaSoft.Simpra.tests/FunctionCallTests.cs
@@ -0,0 +1,97 @@
+using AltaSoft.Simpra.Tests.Models;
+using static AltaSoft.Simpra.Tests.Models.TestModelFactory;
+
+namespace AltaSoft.Simpra.Tests;
+
+public class FunctionCallTests
+{
+ [Fact]
+ public void CallInterfaceFunctionFromSimpra_ShouldReturnCorrectValue()
+ {
+ const string expressionCode =
+ "return Upper('test')";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal("TEST", result);
+ }
+
+ [Fact]
+ public void CallBaseInterfaceFunctionFromSimpra_ShouldReturnCorrectValue()
+ {
+ const string expressionCode =
+ "return Lower('TEST')";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal("test", result);
+ }
+
+ [Fact]
+ public void CallBaseStaticFunctionFromSimpra_ShouldReturnCorrectValue()
+ {
+ const string expressionCode =
+ "return CallBaseStaticMethod()";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal("BaseStaticMethod", result);
+ }
+
+ [Fact]
+ public void CallBaseFunctionFromSimpra_ShouldReturnCorrectValue()
+ {
+ const string expressionCode =
+ "return CallBaseMethod()";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal("BaseMethod", result);
+ }
+
+ [Fact]
+ public void CallFunctionFromSimpra()
+ {
+ const string expressionCode =
+ "return ListSomeCountries('GE')";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute, TestModel, TestFunctions>(model, new TestFunctions(), expressionCode);
+ Assert.True(result.SequenceEqual(["RU", "BE", "GE"]));
+ }
+
+ [Fact]
+ public void Expression_Should_HandleNestedFunctionCallsCorrectly()
+ {
+ const string expressionCode =
+ """
+ let str = 'hello world'
+ let upperStr = Upper(str)
+ return Lower(upperStr) is str
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void CallExternalFunction_WithNonDecimalNumericParameter_ShouldSelectOverloadAndConvertArgument()
+ {
+ const string expressionCode = "return DescribeAsInt(7)";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.Equal("int:7", result);
+ }
+}
diff --git a/tests/AltaSoft.Simpra.tests/Models/TestFunctions.cs b/tests/AltaSoft.Simpra.tests/Models/TestFunctions.cs
new file mode 100644
index 0000000..09bf8ad
--- /dev/null
+++ b/tests/AltaSoft.Simpra.tests/Models/TestFunctions.cs
@@ -0,0 +1,56 @@
+namespace AltaSoft.Simpra.Tests.Models;
+
+public class BaseFunctions
+{
+ public static string CallBaseStaticMethod() => "BaseStaticMethod";
+
+ public static string CallBaseMethod() => "BaseMethod";
+}
+
+public interface IBaseFunctions
+{
+ string Lower(string str);
+}
+
+public interface IFunctions : IBaseFunctions
+{
+ string Upper(string str);
+}
+
+public class TestFunctions : BaseFunctions, IFunctions
+{
+ // ReSharper disable UnusedMember.Global
+ public static string[] ListSomeCountries(string key)
+ {
+ return ["RU", "BE", key];
+ }
+
+ public static int[] ListOfCustomerIds(string key)
+ {
+ return [1, 2];
+ }
+
+#pragma warning disable S2325
+ public string Upper(string str) => str.ToUpper();
+
+ public string Lower(string str) => str.ToLower();
+
+ public ValueTask> ListOfCurrencyCodes(string name) => ValueTask.FromResult(new List() { "EUR", "GEL" });
+
+ public ValueTask ComputeAsync(decimal a, decimal b) => ValueTask.FromResult(a + b);
+
+ public static string DescribeAsInt(int value) => $"int:{value}";
+
+ public string[] List(string key)
+ {
+ return ["RU", "BE"];
+ }
+
+ public ValueTask BigListAsync(string key, CancellationToken cancellationToken)
+#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
+ {
+ return ValueTask.FromResult(new[] { "RU", "BE" });
+ }
+#pragma warning restore S2325
+ // ReSharper restore UnusedMember.Global
+}
diff --git a/tests/AltaSoft.Simpra.tests/Models/TestModelFactory.cs b/tests/AltaSoft.Simpra.tests/Models/TestModelFactory.cs
new file mode 100644
index 0000000..cf802cc
--- /dev/null
+++ b/tests/AltaSoft.Simpra.tests/Models/TestModelFactory.cs
@@ -0,0 +1,9 @@
+namespace AltaSoft.Simpra.Tests.Models;
+
+internal static class TestModelFactory
+{
+ public static TestModel GetTestModel()
+ {
+ return new TestModel { Transfer = new Transfer { Amount = 100, Currency = "USD" }, Customer = new Customer { Id = 1, Status = 1 }, Remittance = "Test" };
+ }
+}
diff --git a/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs b/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs
index caa5f1a..cf4d398 100644
--- a/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs
+++ b/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs
@@ -11,8 +11,8 @@ public void NullDictionary_SingleKeyLookup_IsComparison_ShouldReturnFalse()
var simpra = new Simpra();
var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\"");
Assert.False(result);
@@ -24,8 +24,8 @@ public void NullDictionary_MultipleKeyLookups_AndOperator_ShouldReturnFalse()
var simpra = new Simpra();
var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Properties[\"AccSubType\"] is \"8\"");
Assert.False(result);
@@ -37,8 +37,8 @@ public void NullDictionary_MultipleKeyLookups_OrOperator_ShouldReturnFalse()
var simpra = new Simpra();
var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Properties[\"AccSubType\"] is \"8\"");
Assert.False(result);
@@ -50,8 +50,8 @@ public void NullDictionary_ReturnValue_ShouldReturnNull()
var simpra = new Simpra();
var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"return DebtorAccount.Properties[\"AccType\"]");
Assert.Null(result);
@@ -63,8 +63,8 @@ public void NullDictionary_HasValue_ShouldReturnFalse()
var simpra = new Simpra();
var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] has value");
Assert.False(result);
@@ -84,8 +84,8 @@ public void NonNullDictionary_MissingKey_ShouldReturnFalse()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\"");
Assert.False(result);
@@ -103,8 +103,8 @@ public void NonNullDictionary_ExistingKey_ShouldReturnTrue()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\"");
Assert.True(result);
@@ -122,8 +122,8 @@ public void NonNullDictionary_ExistingKey_ReturnValue()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"return DebtorAccount.Properties[\"AccType\"]");
Assert.Equal("200", result);
@@ -141,8 +141,8 @@ public void NonNullDictionary_KeyWithNullValue_HasValue_ShouldReturnFalse()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] has value");
Assert.False(result);
@@ -156,8 +156,8 @@ public void NullDictionary_ComplexValueType_PropertyAccess_ShouldReturnDefault()
var simpra = new Simpra();
var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"return DebtorAccount.CustomerMap[\"key\"].Id");
Assert.Equal(0, result);
@@ -169,8 +169,8 @@ public void NullDictionary_ComplexValueType_Comparison_ShouldReturnFalse()
var simpra = new Simpra();
var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.CustomerMap[\"key\"].Id is 1");
Assert.False(result);
@@ -190,8 +190,8 @@ public void NonNullDictionary_ComplexValueType_ExistingKey_ShouldReturnCorrectVa
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"return DebtorAccount.CustomerMap[\"vip\"].Id");
Assert.Equal(42, result);
@@ -209,8 +209,8 @@ public void NonNullDictionary_ComplexValueType_MissingKey_ShouldReturnDefault()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"return DebtorAccount.CustomerMap[\"unknown\"].Id");
Assert.Equal(0, result);
@@ -227,8 +227,8 @@ public void NullParentObject_DictionaryAccess_ShouldReturnFalse()
DebtorAccount = new DebtorAccount { Nested = null }
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Nested.Properties[\"Key\"] is \"value\"");
Assert.False(result);
@@ -243,8 +243,8 @@ public void NullParentObject_DictionaryAccess_ReturnValue_ShouldReturnNull()
DebtorAccount = new DebtorAccount { Nested = null }
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"return DebtorAccount.Nested.Properties[\"Key\"]");
Assert.Null(result);
@@ -264,8 +264,8 @@ public void NestedObject_NullDictionary_ShouldReturnFalse()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Nested.Properties[\"Key\"] is \"value\"");
Assert.False(result);
@@ -286,8 +286,8 @@ public void NestedObject_NonNullDictionary_ExistingKey_ShouldReturnTrue()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Nested.Properties[\"Key\"] is \"value\"");
Assert.True(result);
@@ -304,8 +304,8 @@ public void NullList_IndexAccess_ReturnValue_ShouldReturnNull()
DebtorAccount = new DebtorAccount { Tags = null }
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"return DebtorAccount.Tags[1]");
Assert.Null(result);
@@ -320,8 +320,8 @@ public void NullList_IndexAccess_Comparison_ShouldReturnFalse()
DebtorAccount = new DebtorAccount { Tags = null }
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Tags[1] is \"hello\"");
Assert.False(result);
@@ -338,8 +338,8 @@ public void NullProperty_MemberAccess_Comparison_ShouldReturnFalse()
DebtorAccount = new DebtorAccount { Name = null }
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Name is \"Test\"");
Assert.False(result);
@@ -354,8 +354,8 @@ public void NullProperty_MemberAccess_HasValue_ShouldReturnFalse()
DebtorAccount = new DebtorAccount { Name = null }
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Name has value");
Assert.False(result);
@@ -370,8 +370,8 @@ public void NonNullProperty_MemberAccess_HasValue_ShouldReturnTrue()
DebtorAccount = new DebtorAccount { Name = "Test" }
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Name has value");
Assert.True(result);
@@ -388,8 +388,8 @@ public void NullDictionary_And_NonNullProperty_MixedExpression_ShouldReturnFalse
DebtorAccount = new DebtorAccount { Name = "Test" }
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Name is \"Test\"");
Assert.False(result);
@@ -404,8 +404,8 @@ public void NullDictionary_Or_NonNullProperty_MixedExpression_ShouldReturnTrue()
DebtorAccount = new DebtorAccount { Name = "Test" }
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Name is \"Test\"");
Assert.True(result);
@@ -419,8 +419,8 @@ public void NullDictionary_IsNotComparison_ShouldReturnTrue()
var simpra = new Simpra();
var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is not \"200\"");
Assert.True(result);
@@ -434,8 +434,8 @@ public void NullDictionary_InOperator_ShouldReturnFalse()
var simpra = new Simpra();
var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] in [\"200\", \"300\"]");
Assert.False(result);
@@ -449,8 +449,8 @@ public void NullDictionary_WhenExpression_ShouldEvaluateElseBranch()
var simpra = new Simpra();
var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"return when DebtorAccount.Properties[\"AccType\"] is \"200\" then \"match\" else \"no match\"");
Assert.Equal("no match", result);
@@ -470,8 +470,8 @@ public void TestModel_NullCountriesDictionary_ShouldReturnFalse()
Countries = null
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"Countries[\"Georgia\"] is \"Test\"");
Assert.False(result);
@@ -489,8 +489,8 @@ public void TestModel_NullCountriesDictionary_ReturnValue_ShouldReturnNull()
Countries = null
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"return Countries[\"Georgia\"]");
Assert.Null(result);
@@ -508,8 +508,8 @@ public void TestModel_NullDictionaryOfObjects_PropertyAccess_ShouldReturnDefault
DictionaryOfObjects = null
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"return DictionaryOfObjects[\"test\"].Id");
Assert.Equal(0, result);
@@ -527,8 +527,8 @@ public void TestModel_NullDictionaryOfObjects_Comparison_ShouldReturnFalse()
DictionaryOfObjects = null
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DictionaryOfObjects[\"test\"].Id is 1");
Assert.False(result);
@@ -547,8 +547,8 @@ public void NullTransfer_PropertyAccess_ShouldReturnDefault()
Remittance = "Test"
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"return Transfer.Amount");
Assert.Equal(0m, result);
@@ -565,8 +565,8 @@ public void NullTransfer_CurrencyComparison_ShouldReturnFalse()
Remittance = "Test"
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"Transfer.Currency is \"USD\"");
Assert.False(result);
@@ -583,8 +583,8 @@ public void NullTransfer_HasValue_ShouldReturnFalse()
Remittance = "Test"
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"Transfer has value");
Assert.False(result);
@@ -605,8 +605,8 @@ public void BothDictsPopulated_And_BothMatch_ShouldReturnTrue()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\"");
Assert.True(result);
@@ -625,8 +625,8 @@ public void BothDictsPopulated_And_OneMismatch_ShouldReturnFalse()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\"");
Assert.False(result);
@@ -645,8 +645,8 @@ public void BothDictsPopulated_Or_OneMismatch_ShouldReturnTrue()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\"");
Assert.True(result);
@@ -665,8 +665,8 @@ public void BothDictsPopulated_Or_BothMismatch_ShouldReturnFalse()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\"");
Assert.False(result);
@@ -685,8 +685,8 @@ public void PropertiesNull_AttributesPopulated_And_ShouldReturnFalse()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\"");
Assert.False(result);
@@ -705,8 +705,8 @@ public void PropertiesNull_AttributesPopulated_Or_ShouldReturnTrue()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\"");
Assert.True(result);
@@ -725,8 +725,8 @@ public void PropertiesPopulated_AttributesNull_And_ShouldReturnFalse()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\"");
Assert.False(result);
@@ -745,8 +745,8 @@ public void PropertiesPopulated_AttributesNull_Or_ShouldReturnTrue()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\"");
Assert.True(result);
@@ -758,8 +758,8 @@ public void BothDictsNull_And_ShouldReturnFalse()
var simpra = new Simpra();
var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\"");
Assert.False(result);
@@ -771,8 +771,8 @@ public void BothDictsNull_Or_ShouldReturnFalse()
var simpra = new Simpra();
var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\"");
Assert.False(result);
@@ -791,8 +791,8 @@ public void BothDictsPopulated_MixedMissingKeys_And_ShouldReturnFalse()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\"");
Assert.False(result);
@@ -811,8 +811,8 @@ public void BothDictsPopulated_MixedMissingKeys_Or_ShouldReturnTrue()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\"");
Assert.True(result);
@@ -832,8 +832,8 @@ public void ThreeConditions_And_AllMatch_ShouldReturnTrue()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\" and DebtorAccount.Name is \"VIP\"");
Assert.True(result);
@@ -853,8 +853,8 @@ public void ThreeConditions_And_MiddleNull_ShouldReturnFalse()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\" and DebtorAccount.Name is \"VIP\"");
Assert.False(result);
@@ -874,8 +874,8 @@ public void ThreeConditions_Or_OnlyLastMatches_ShouldReturnTrue()
}
};
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\" or DebtorAccount.Name is \"VIP\"");
Assert.True(result);
@@ -896,8 +896,8 @@ public void MixedAndOr_NullDict_And_PopulatedDict_Or_Property_ShouldEvaluateCorr
};
// (null_dict is "200" and attr is "EU") or name is "VIP" → (false and true) or true → true
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"(DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\") or DebtorAccount.Name is \"VIP\"");
Assert.True(result);
@@ -918,8 +918,8 @@ public void MixedAndOr_NullDict_Or_PopulatedDict_And_Property_ShouldEvaluateCorr
};
// (null_dict is "200" or attr is "EU") and name is "VIP" → (false or true) and true → true
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"(DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\") and DebtorAccount.Name is \"VIP\"");
Assert.True(result);
@@ -940,8 +940,8 @@ public void MixedAndOr_AllNull_And_Property_ShouldReturnFalse()
};
// (null is "200" or null is "EU") and null_name is "VIP" → (false or false) and false → false
- var result = simpra.Execute(
- model, new SimpraExpressionTests.TestFunctions(),
+ var result = simpra.Execute(
+ model, new TestFunctions(),
"(DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\") and DebtorAccount.Name is \"VIP\"");
Assert.False(result);
diff --git a/tests/AltaSoft.Simpra.tests/OperatorTests.cs b/tests/AltaSoft.Simpra.tests/OperatorTests.cs
new file mode 100644
index 0000000..7abe8d6
--- /dev/null
+++ b/tests/AltaSoft.Simpra.tests/OperatorTests.cs
@@ -0,0 +1,698 @@
+using AltaSoft.Simpra.Tests.Models;
+using static AltaSoft.Simpra.Tests.Models.TestModelFactory;
+
+namespace AltaSoft.Simpra.Tests;
+
+public class OperatorTests
+{
+ [Fact]
+ public void TaskExpression_Should_ReturnTrue_When_ValueMatchesRegexPattern()
+ {
+ const string expressionCode =
+ """
+ let Value = 'nbas568fq'
+ return Value matches '[a-zA-Z_][a-zA-Z_0-9]'
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode,
+ new SimpraCompilerOptions { MutabilityOption = MutabilityOption.Immutable, StringComparisonOption = StringComparisonOption.IgnoreCase });
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Execute_NullableAmount_LessThanTen_ShouldReturnTrue()
+ {
+ const string expressionCode = "NullableAmount < 10";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, expressionCode, null);
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Execute_NullableIntegerProperty_LessThanZero_ShouldReturnFalse()
+ {
+ const string expressionCode = "NullableIntegerProperty < 0";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+
+ var result = simpra.Execute(model, expressionCode, null);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ExecuteExpression_Should_ReturnFalse_When_TransferAmountIsGreaterThanTen()
+ {
+ const string expressionCode = "Transfer.Amount < 10";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer!.Amount = 11;
+
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ExecuteExpression_WithLogicalOperations_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new TestModelMain { Test = null };
+ const string expression = "return (false or false) and (true or true)";
+
+ var result = simpra.Execute(model, new TestFunctions(), expression);
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ExecuteExpression_WithArithmeticOperations_ShouldReturnCorrectValue()
+ {
+ var simpra = new Simpra();
+ var model = new TestModelMain { Test = null };
+ const string expression = "return (10 + 10) * 10";
+ const int expected = 200;
+
+ var result = simpra.Execute(model, new TestFunctions(), expression);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ExecuteExpression_Should_ReturnZero_When_AddingOneAndNegativeOne()
+ {
+ const string expression = """
+ let x = 1
+ let y = -1
+ return x + y
+ """;
+ var simpra = new Simpra();
+ var model = new TestModelMain { Test = new TestModel1 { Test = TestModel2.Test2 } };
+ var result = simpra.Execute(model, new TestFunctions(), expression);
+ Assert.Equal(0, result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_StrMatchesPattern()
+ {
+ const string expressionCode =
+ """
+ let str = 'abc';
+ return str like 'a%'
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel(); // Model is irrelevant here
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_CaseInsensitivePatternMatches()
+ {
+ const string expressionCode =
+ """
+ let str = 'abc'
+ $case_sensitive off
+ return str like 'A%'
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel(); // Model is irrelevant here
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnFalse_When_CaseSensitivePatternDoesNotMatch()
+ {
+ const string expressionCode =
+ """
+ let str = 'abc'
+ $case_sensitive on
+ return str like 'A%'
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel(); // Model is irrelevant here
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_AmountIsGreaterThanHundred()
+ {
+ const string expressionCode =
+ """
+ let transfer = Transfer
+ let amount = transfer.Amount
+ return amount > 100 * 2 / 1.1
+ """;
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer!.Amount = 200;
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_AmountIsGreaterThanFifty_And_CurrencyIsUSD()
+ {
+ const string expressionCode = "return Transfer.Amount > 50 and Transfer.Currency is 'USD'";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnFalse_When_AmountIsNotGreaterThanFifty_And_CurrencyIsNotUSD()
+ {
+ const string expressionCode = "return not (Transfer.Amount > 50 and Transfer.Currency is 'USD')";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_CurrencyIsInList()
+ {
+ const string expressionCode = "return Transfer.Currency in ['USD', 'EUR']";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnFalse_When_CurrencyIsNotInList()
+ {
+ const string expressionCode = "return Transfer.Currency not in ['USD', 'EUR']";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_CurrencyIsConcatenatedString()
+ {
+ const string expressionCode = "return Transfer.Currency is 'US' + 'D'";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_CurrencyIsInConcatenatedList()
+ {
+ const string expressionCode = "return Transfer.Currency in ['USD', 'EUR'] + 'GEL'";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_AmountIsEqualToFifty()
+ {
+ const string expressionCode = "return Transfer.Amount is 50";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer!.Amount = 50;
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_NestedConditionsAreMet()
+ {
+ const string expressionCode =
+ "return (Transfer.Amount > 50 and Transfer.Currency is 'USD') or (Transfer.Amount < 20 and Transfer.Currency is 'EUR')";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_CurrencyIsEmpty()
+ {
+ const string expressionCode = "return Transfer.Currency is ''";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ model.Transfer!.Currency = "";
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnHelloWorld_When_StringsAreConcatenated()
+ {
+ const string expressionCode = "return 'Hello' + ' ' + 'World!'";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal("Hello World!", result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnCurrencyWithString_When_Concatenated()
+ {
+ const string expressionCode = "return Transfer.Currency + ' is strong'";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal("USD is strong", result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnCurrencyAndAmount_When_Concatenated()
+ {
+ const string expressionCode = "return Transfer.Currency + ' - Amount: ' + Transfer.Amount";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute(model, new TestFunctions(), expressionCode);
+
+ Assert.Equal("USD - Amount: 100", result);
+ }
+
+ [Fact]
+ public void Expression_Should_ReturnTrue_When_CurrencyIsInSubtractedList()
+ {
+ const string expressionCode = "return Transfer.Currency in ['USD', 'EUR', 'GEL'] - ['GEL']";
+
+ var simpra = new Simpra();
+ var model = GetTestModel();
+ var result = simpra.Execute