diff --git a/Directory.Build.props b/Directory.Build.props
index 9d26598..ceb7f23 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -11,7 +11,7 @@
Simpra
ALTA Software llc.
Copyright © 2024 ALTA Software llc.
- 2.0.1
+ 2.0.2
diff --git a/Directory.Packages.props b/Directory.Packages.props
index b78452f..357e003 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,14 +4,14 @@
-
-
+
+
-
+
-
+
\ No newline at end of file
diff --git a/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs b/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs
index 316616c..433a79b 100644
--- a/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs
+++ b/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs
@@ -569,11 +569,18 @@ private static Expression HandleDictionaryIndexAccess(SimpraParser.IndexAccessCo
var keyExpr = index.Type != keyType ? Expression.Convert(index, keyType) : index;
var valueVar = Expression.Variable(valueType, "dictValue");
+ var dictVar = Expression.Variable(objectType, "dictRef");
+ var assignDict = Expression.Assign(dictVar, left);
- var tryGetValueCall = Expression.Call(left, tryGetValueMethod, keyExpr, valueVar);
+ var tryGetValueCall = Expression.Call(dictVar, tryGetValueMethod, keyExpr, valueVar);
+ var notNull = Expression.NotEqual(dictVar, Expression.Constant(null, objectType));
var defaultValue = Expression.Default(valueType);
- var block = Expression.Block([valueVar], Expression.Condition(tryGetValueCall, valueVar, defaultValue));
+ var block = Expression.Block(
+ [dictVar, valueVar],
+ assignDict,
+ Expression.Condition(Expression.AndAlso(notNull, tryGetValueCall), valueVar, defaultValue)
+ );
return ConvertToSimpraType(block, false, context);
}
diff --git a/tests/AltaSoft.Simpra.tests/Models/DebtorAccount.cs b/tests/AltaSoft.Simpra.tests/Models/DebtorAccount.cs
new file mode 100644
index 0000000..ba6af27
--- /dev/null
+++ b/tests/AltaSoft.Simpra.tests/Models/DebtorAccount.cs
@@ -0,0 +1,17 @@
+namespace AltaSoft.Simpra.Tests.Models;
+
+public sealed class DebtorAccountModel
+{
+ public required DebtorAccount DebtorAccount { get; set; }
+}
+
+public sealed class DebtorAccount
+{
+ public Dictionary? Properties { get; set; }
+ public Dictionary? Attributes { get; set; }
+ public Dictionary? IntKeyedProperties { get; set; }
+ public Dictionary? CustomerMap { get; set; }
+ public DebtorAccount? Nested { get; set; }
+ public string? Name { get; set; }
+ public List? Tags { get; set; }
+}
diff --git a/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs b/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs
new file mode 100644
index 0000000..caa5f1a
--- /dev/null
+++ b/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs
@@ -0,0 +1,949 @@
+using AltaSoft.Simpra.Tests.Models;
+
+namespace AltaSoft.Simpra.Tests;
+
+public class NullSafetyTests
+{
+
+ [Fact]
+ 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(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ 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(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Properties[\"AccSubType\"] is \"8\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ 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(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Properties[\"AccSubType\"] is \"8\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void NullDictionary_ReturnValue_ShouldReturnNull()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "return DebtorAccount.Properties[\"AccType\"]");
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void NullDictionary_HasValue_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] has value");
+
+ Assert.False(result);
+ }
+
+ // ── Non-null dictionary: missing key should return default ──
+
+ [Fact]
+ public void NonNullDictionary_MissingKey_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = new Dictionary { { "Other", "value" } }
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void NonNullDictionary_ExistingKey_ShouldReturnTrue()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = new Dictionary { { "AccType", "200" } }
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\"");
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void NonNullDictionary_ExistingKey_ReturnValue()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = new Dictionary { { "AccType", "200" } }
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "return DebtorAccount.Properties[\"AccType\"]");
+
+ Assert.Equal("200", result);
+ }
+
+ [Fact]
+ public void NonNullDictionary_KeyWithNullValue_HasValue_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = new Dictionary { { "AccType", null } }
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] has value");
+
+ Assert.False(result);
+ }
+
+ // ── Null dictionary with complex value type ──
+
+ [Fact]
+ 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(),
+ "return DebtorAccount.CustomerMap[\"key\"].Id");
+
+ Assert.Equal(0, result);
+ }
+
+ [Fact]
+ 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(),
+ "DebtorAccount.CustomerMap[\"key\"].Id is 1");
+
+ Assert.False(result);
+ }
+
+ // ── Non-null dict with complex values – existing vs missing key ──
+
+ [Fact]
+ public void NonNullDictionary_ComplexValueType_ExistingKey_ShouldReturnCorrectValue()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ CustomerMap = new Dictionary { { "vip", new Customer { Id = 42, Status = 10 } } }
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "return DebtorAccount.CustomerMap[\"vip\"].Id");
+
+ Assert.Equal(42, result);
+ }
+
+ [Fact]
+ public void NonNullDictionary_ComplexValueType_MissingKey_ShouldReturnDefault()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ CustomerMap = new Dictionary { { "vip", new Customer { Id = 42, Status = 10 } } }
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "return DebtorAccount.CustomerMap[\"unknown\"].Id");
+
+ Assert.Equal(0, result);
+ }
+
+ // ── Null parent object in chain before dictionary ──
+
+ [Fact]
+ public void NullParentObject_DictionaryAccess_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount { Nested = null }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Nested.Properties[\"Key\"] is \"value\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void NullParentObject_DictionaryAccess_ReturnValue_ShouldReturnNull()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount { Nested = null }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "return DebtorAccount.Nested.Properties[\"Key\"]");
+
+ Assert.Null(result);
+ }
+
+ // ── Nested dictionary on non-null parent ──
+
+ [Fact]
+ public void NestedObject_NullDictionary_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Nested = new DebtorAccount() // Nested.Properties is null
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Nested.Properties[\"Key\"] is \"value\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void NestedObject_NonNullDictionary_ExistingKey_ShouldReturnTrue()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Nested = new DebtorAccount
+ {
+ Properties = new Dictionary { { "Key", "value" } }
+ }
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Nested.Properties[\"Key\"] is \"value\"");
+
+ Assert.True(result);
+ }
+
+ // ── Null list access (non-dictionary indexable) ──
+
+ [Fact]
+ public void NullList_IndexAccess_ReturnValue_ShouldReturnNull()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount { Tags = null }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "return DebtorAccount.Tags[1]");
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void NullList_IndexAccess_Comparison_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount { Tags = null }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Tags[1] is \"hello\"");
+
+ Assert.False(result);
+ }
+
+ // ── Null property on parent (non-dictionary, non-list) ──
+
+ [Fact]
+ public void NullProperty_MemberAccess_Comparison_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount { Name = null }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Name is \"Test\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void NullProperty_MemberAccess_HasValue_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount { Name = null }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Name has value");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void NonNullProperty_MemberAccess_HasValue_ShouldReturnTrue()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount { Name = "Test" }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Name has value");
+
+ Assert.True(result);
+ }
+
+ // ── Mixed: null dict combined with non-null property in same expression ──
+
+ [Fact]
+ public void NullDictionary_And_NonNullProperty_MixedExpression_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount { Name = "Test" }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Name is \"Test\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void NullDictionary_Or_NonNullProperty_MixedExpression_ShouldReturnTrue()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount { Name = "Test" }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Name is \"Test\"");
+
+ Assert.True(result);
+ }
+
+ // ── Null dictionary with "is not" operator ──
+
+ [Fact]
+ public void NullDictionary_IsNotComparison_ShouldReturnTrue()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is not \"200\"");
+
+ Assert.True(result);
+ }
+
+ // ── Null dictionary with "in" operator ──
+
+ [Fact]
+ public void NullDictionary_InOperator_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] in [\"200\", \"300\"]");
+
+ Assert.False(result);
+ }
+
+ // ── Null dictionary in when/conditional expressions ──
+
+ [Fact]
+ public void NullDictionary_WhenExpression_ShouldEvaluateElseBranch()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "return when DebtorAccount.Properties[\"AccType\"] is \"200\" then \"match\" else \"no match\"");
+
+ Assert.Equal("no match", result);
+ }
+
+ // ── Existing dictionary tests (TestModel.Countries) with null dict ──
+
+ [Fact]
+ public void TestModel_NullCountriesDictionary_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new TestModel
+ {
+ Transfer = new Transfer { Amount = 100, Currency = "USD" },
+ Customer = new Customer { Id = 1, Status = 1 },
+ Remittance = "Test",
+ Countries = null
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "Countries[\"Georgia\"] is \"Test\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void TestModel_NullCountriesDictionary_ReturnValue_ShouldReturnNull()
+ {
+ var simpra = new Simpra();
+ var model = new TestModel
+ {
+ Transfer = new Transfer { Amount = 100, Currency = "USD" },
+ Customer = new Customer { Id = 1, Status = 1 },
+ Remittance = "Test",
+ Countries = null
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "return Countries[\"Georgia\"]");
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void TestModel_NullDictionaryOfObjects_PropertyAccess_ShouldReturnDefault()
+ {
+ var simpra = new Simpra();
+ var model = new TestModel
+ {
+ Transfer = new Transfer { Amount = 100, Currency = "USD" },
+ Customer = new Customer { Id = 1, Status = 1 },
+ Remittance = "Test",
+ DictionaryOfObjects = null
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "return DictionaryOfObjects[\"test\"].Id");
+
+ Assert.Equal(0, result);
+ }
+
+ [Fact]
+ public void TestModel_NullDictionaryOfObjects_Comparison_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new TestModel
+ {
+ Transfer = new Transfer { Amount = 100, Currency = "USD" },
+ Customer = new Customer { Id = 1, Status = 1 },
+ Remittance = "Test",
+ DictionaryOfObjects = null
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DictionaryOfObjects[\"test\"].Id is 1");
+
+ Assert.False(result);
+ }
+
+ // ── Null Transfer (parent object) with sub-property access ──
+
+ [Fact]
+ public void NullTransfer_PropertyAccess_ShouldReturnDefault()
+ {
+ var simpra = new Simpra();
+ var model = new TestModel
+ {
+ Transfer = null,
+ Customer = new Customer { Id = 1, Status = 1 },
+ Remittance = "Test"
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "return Transfer.Amount");
+
+ Assert.Equal(0m, result);
+ }
+
+ [Fact]
+ public void NullTransfer_CurrencyComparison_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new TestModel
+ {
+ Transfer = null,
+ Customer = new Customer { Id = 1, Status = 1 },
+ Remittance = "Test"
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "Transfer.Currency is \"USD\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void NullTransfer_HasValue_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new TestModel
+ {
+ Transfer = null,
+ Customer = new Customer { Id = 1, Status = 1 },
+ Remittance = "Test"
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "Transfer has value");
+
+ Assert.False(result);
+ }
+
+ // ── Cross-dictionary and/or logic: Properties + Attributes ──
+
+ [Fact]
+ public void BothDictsPopulated_And_BothMatch_ShouldReturnTrue()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = new Dictionary { { "AccType", "200" } },
+ Attributes = new Dictionary { { "Region", "EU" } }
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\"");
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void BothDictsPopulated_And_OneMismatch_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = new Dictionary { { "AccType", "200" } },
+ Attributes = new Dictionary { { "Region", "US" } }
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void BothDictsPopulated_Or_OneMismatch_ShouldReturnTrue()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = new Dictionary { { "AccType", "200" } },
+ Attributes = new Dictionary { { "Region", "US" } }
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\"");
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void BothDictsPopulated_Or_BothMismatch_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = new Dictionary { { "AccType", "100" } },
+ Attributes = new Dictionary { { "Region", "US" } }
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void PropertiesNull_AttributesPopulated_And_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = null,
+ Attributes = new Dictionary { { "Region", "EU" } }
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void PropertiesNull_AttributesPopulated_Or_ShouldReturnTrue()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = null,
+ Attributes = new Dictionary { { "Region", "EU" } }
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\"");
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void PropertiesPopulated_AttributesNull_And_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = new Dictionary { { "AccType", "200" } },
+ Attributes = null
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void PropertiesPopulated_AttributesNull_Or_ShouldReturnTrue()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = new Dictionary { { "AccType", "200" } },
+ Attributes = null
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\"");
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void BothDictsNull_And_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void BothDictsNull_Or_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void BothDictsPopulated_MixedMissingKeys_And_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = new Dictionary { { "AccType", "200" } },
+ Attributes = new Dictionary { { "Other", "X" } }
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void BothDictsPopulated_MixedMissingKeys_Or_ShouldReturnTrue()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = new Dictionary { { "AccType", "200" } },
+ Attributes = new Dictionary { { "Other", "X" } }
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\"");
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ThreeConditions_And_AllMatch_ShouldReturnTrue()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = new Dictionary { { "AccType", "200" } },
+ Attributes = new Dictionary { { "Region", "EU" } },
+ Name = "VIP"
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\" and DebtorAccount.Name is \"VIP\"");
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ThreeConditions_And_MiddleNull_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = new Dictionary { { "AccType", "200" } },
+ Attributes = null,
+ Name = "VIP"
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\" and DebtorAccount.Name is \"VIP\"");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ThreeConditions_Or_OnlyLastMatches_ShouldReturnTrue()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = null,
+ Attributes = null,
+ Name = "VIP"
+ }
+ };
+
+ var result = simpra.Execute(
+ model, new SimpraExpressionTests.TestFunctions(),
+ "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\" or DebtorAccount.Name is \"VIP\"");
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void MixedAndOr_NullDict_And_PopulatedDict_Or_Property_ShouldEvaluateCorrectly()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = null,
+ Attributes = new Dictionary { { "Region", "EU" } },
+ Name = "VIP"
+ }
+ };
+
+ // (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(),
+ "(DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\") or DebtorAccount.Name is \"VIP\"");
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void MixedAndOr_NullDict_Or_PopulatedDict_And_Property_ShouldEvaluateCorrectly()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = null,
+ Attributes = new Dictionary { { "Region", "EU" } },
+ Name = "VIP"
+ }
+ };
+
+ // (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(),
+ "(DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\") and DebtorAccount.Name is \"VIP\"");
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void MixedAndOr_AllNull_And_Property_ShouldReturnFalse()
+ {
+ var simpra = new Simpra();
+ var model = new DebtorAccountModel
+ {
+ DebtorAccount = new DebtorAccount
+ {
+ Properties = null,
+ Attributes = null,
+ Name = null
+ }
+ };
+
+ // (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(),
+ "(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/SimpraExpressionTests.cs b/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs
index 2e8a1f5..d21aad2 100644
--- a/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs
+++ b/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs
@@ -1,4 +1,4 @@
-using AltaSoft.Simpra.Tests.Models;
+using AltaSoft.Simpra.Tests.Models;
namespace AltaSoft.Simpra.Tests;
@@ -28,12 +28,12 @@ public class SimpraExpressionTests
[InlineData("USD", 2, 0, "")]
[InlineData("USD", 3, 0, "")]
- // negative/zero start → clamp to 1
+ // negative/zero start ? clamp to 1
[InlineData("USD", 0, 1, "U")]
[InlineData("USD", -5, 2, "US")]
[InlineData("USD", -2, 100, "USD")]
- // negative length → empty
+ // negative length ? empty
[InlineData("USD", 1, -1, "")]
[InlineData("USD", 3, -10, "")]
[InlineData("USD", -2, -10, "")]
@@ -50,10 +50,10 @@ public class SimpraExpressionTests
[InlineData(" ", 2, 100, " ")]
// non-ASCII (safe checks with multi-byte chars)
- [InlineData("თბილისი", 1, 2, "თბ")]
- [InlineData("თბილისი", 3, 3, "ილი")]
- [InlineData("თბილისი", 11, 5, "")]
- [InlineData("თბილისი", -3, 100, "თბილისი")]
+ [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)
{
@@ -101,7 +101,7 @@ public void BuiltInFunction_Substring_ShouldMatchExamplesFromPrompt()
[InlineData("", 1, "")]
[InlineData("", 5, "")]
- // zero/negative start → clamp to 1
+ // zero/negative start ? clamp to 1
[InlineData("USD", 0, "U")]
[InlineData("USD", -1, "U")]
[InlineData("USD", -5, "U")]
@@ -113,11 +113,11 @@ public void BuiltInFunction_Substring_ShouldMatchExamplesFromPrompt()
[InlineData(" X ", 3, " ")]
// non-ASCII (single UTF-16 code units)
- [InlineData("თბილისი", 1, "თ")]
- [InlineData("თბილისი", 3, "ი")]
- [InlineData("თბილისი", 6, "ს")]
- [InlineData("თბილისი", 7, "ი")]
- [InlineData("თბილისი", 8, "")]
+ [InlineData("???????", 1, "?")]
+ [InlineData("???????", 3, "?")]
+ [InlineData("???????", 6, "?")]
+ [InlineData("???????", 7, "?")]
+ [InlineData("???????", 8, "")]
// very large index
[InlineData("USD", int.MaxValue, "")]