Skip to content

Commit bef468f

Browse files
committed
chore: deeply nested property updates
1 parent 22bc4fc commit bef468f

3 files changed

Lines changed: 161 additions & 4 deletions

File tree

ObjectSemantics.NET.Tests/CognitiveMapTests.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,83 @@ public void Library_Entry_Point_String_Extension_Should_Work(string personName)
3333
Assert.Equal($"I am {personName}!", generatedTemplate);
3434
}
3535

36+
[Fact]
37+
public void Property_Of_Object_Should_Be_Mapped()
38+
{
39+
CustomerPayment customerPayment = new CustomerPayment
40+
{
41+
Amount = 100_000_000,
42+
Customer = new Customer
43+
{
44+
CompanyName = "CRUDSOFT TECHNOLOGIES"
45+
}
46+
};
47+
string generatedTemplate = "Paid Amount: {{ Amount:N2 }} By {{ Customer.CompanyName }}".Map(customerPayment);
48+
Assert.Equal("Paid Amount: 100,000,000.00 By CRUDSOFT TECHNOLOGIES", generatedTemplate);
49+
}
50+
51+
[Fact]
52+
public void Nested_Property_Should_Be_Empty_When_Parent_Object_Is_Null()
53+
{
54+
CustomerPayment customerPayment = new CustomerPayment
55+
{
56+
Amount = 100_000_000,
57+
Customer = null
58+
};
59+
60+
string generatedTemplate = "Paid Amount: {{ Amount:N2 }} By {{ Customer.CompanyName }}".Map(customerPayment);
61+
Assert.Equal("Paid Amount: 100,000,000.00 By ", generatedTemplate);
62+
}
63+
64+
[Fact]
65+
public void Nested_Property_Should_Be_Empty_When_Target_Property_Is_Null()
66+
{
67+
CustomerPayment customerPayment = new CustomerPayment
68+
{
69+
Amount = 100_000_000,
70+
Customer = new Customer
71+
{
72+
CompanyName = null
73+
}
74+
};
75+
76+
string generatedTemplate = "Paid Amount: {{ Amount:N2 }} By {{ Customer.CompanyName }}".Map(customerPayment);
77+
Assert.Equal("Paid Amount: 100,000,000.00 By ", generatedTemplate);
78+
}
79+
80+
[Fact]
81+
public void Deeply_Nested_Property_Should_Be_Mapped()
82+
{
83+
CustomerPayment customerPayment = new CustomerPayment
84+
{
85+
Amount = 100_000_000,
86+
Customer = new Customer
87+
{
88+
BankingDetail = new CustomerBankingDetail
89+
{
90+
BankName = "KCB BANK"
91+
}
92+
}
93+
};
94+
95+
string generatedTemplate = "Paid Amount: {{ Amount:N2 }} Via {{ Customer.BankingDetail.BankName }}".Map(customerPayment);
96+
Assert.Equal("Paid Amount: 100,000,000.00 Via KCB BANK", generatedTemplate);
97+
}
98+
99+
[Fact]
100+
public void Deeply_Nested_Property_Should_Be_Empty_When_Object_Is_Null()
101+
{
102+
CustomerPayment customerPayment = new CustomerPayment
103+
{
104+
Amount = 100_000_000,
105+
Customer = null
106+
};
107+
108+
string generatedTemplate = "Paid Amount: {{ Amount:N2 }} Via {{ Customer.BankingDetail.BankName }}".Map(customerPayment);
109+
Assert.Equal("Paid Amount: 100,000,000.00 Via ", generatedTemplate);
110+
}
111+
112+
36113
[Fact]
37114
public void Additional_Headers_Should_Also_Be_Mapped()
38115
{

ObjectSemantics.NET.Tests/MoqModels/CustomerPayment.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,12 @@ public class Customer
2424
public string FirstName { get; set; }
2525
public string LastName { get; set; }
2626
public string CompanyName { get; set; }
27+
public CustomerBankingDetail BankingDetail { get; set; }
28+
}
29+
30+
public class CustomerBankingDetail
31+
{
32+
public string BankName { get; set; }
33+
public string AccountNumber { get; set; }
2734
}
2835
}

ObjectSemantics.NET/Engine/EngineAlgorithim.cs

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ internal static class EngineAlgorithim
2929
// ---- IF Conditions ----
3030
foreach (ReplaceIfOperationCode ifCondition in template.ReplaceIfConditionCodes)
3131
{
32-
if (!propMap.TryGetValue(ifCondition.IfPropertyName, out ExtractedObjProperty property))
32+
if (!TryResolveProperty(propMap, ifCondition.IfPropertyName, out ExtractedObjProperty property))
3333
{
3434
result.ReplaceFirstOccurrence(ifCondition.ReplaceRef, "[IF-CONDITION EXCEPTION]: unrecognized property: [" + ifCondition.IfPropertyName + "]");
3535
continue;
@@ -89,7 +89,7 @@ internal static class EngineAlgorithim
8989
}
9090
else
9191
{
92-
if (rowMap.TryGetValue(propName, out ExtractedObjProperty p))
92+
if (TryResolveProperty(rowMap, propName, out ExtractedObjProperty p))
9393
activeRow.ReplaceFirstOccurrence(objLoopCode.ReplaceRef, p.GetPropertyDisplayString(objLoopCode.GetFormattingCommand(), options));
9494
else
9595
activeRow.ReplaceFirstOccurrence(objLoopCode.ReplaceRef, objLoopCode.ReplaceCommand);
@@ -105,7 +105,7 @@ internal static class EngineAlgorithim
105105
// ---- Direct Replacements ----
106106
foreach (ReplaceCode replaceCode in template.ReplaceCodes)
107107
{
108-
if (propMap.TryGetValue(replaceCode.GetTargetPropertyName(), out ExtractedObjProperty property))
108+
if (TryResolveProperty(propMap, replaceCode.GetTargetPropertyName(), out ExtractedObjProperty property))
109109
result.ReplaceFirstOccurrence(replaceCode.ReplaceRef, property.GetPropertyDisplayString(replaceCode.GetFormattingCommand(), options));
110110
else
111111
result.ReplaceFirstOccurrence(replaceCode.ReplaceRef, "{{ " + replaceCode.ReplaceCommand + " }}");
@@ -236,5 +236,78 @@ private static List<ExtractedObjProperty> GetObjPropertiesFromUnknown(object val
236236

237237
return result;
238238
}
239+
240+
private static bool TryResolveProperty(Dictionary<string, ExtractedObjProperty> propMap, string propertyPath, out ExtractedObjProperty result)
241+
{
242+
result = null;
243+
if (propMap == null || string.IsNullOrWhiteSpace(propertyPath))
244+
return false;
245+
246+
string path = propertyPath.Trim();
247+
if (propMap.TryGetValue(path, out result))
248+
return true;
249+
250+
int dotIndex = path.IndexOf('.');
251+
if (dotIndex < 0)
252+
return false;
253+
254+
string rootName = path.Substring(0, dotIndex).Trim();
255+
string nestedPath = path.Substring(dotIndex + 1).Trim();
256+
if (string.IsNullOrEmpty(rootName) || string.IsNullOrEmpty(nestedPath))
257+
return false;
258+
259+
if (!propMap.TryGetValue(rootName, out ExtractedObjProperty rootProperty))
260+
return false;
261+
262+
return TryResolveNestedProperty(rootProperty, nestedPath, path, out result);
263+
}
264+
265+
private static bool TryResolveNestedProperty(ExtractedObjProperty rootProperty, string nestedPath, string fullPath, out ExtractedObjProperty result)
266+
{
267+
result = null;
268+
if (rootProperty == null || string.IsNullOrWhiteSpace(nestedPath))
269+
return false;
270+
271+
string[] segments = nestedPath.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
272+
if (segments.Length == 0)
273+
return false;
274+
275+
object currentValue = rootProperty.OriginalValue;
276+
Type currentType = rootProperty.Type;
277+
278+
for (int i = 0; i < segments.Length; i++)
279+
{
280+
if (currentType == null)
281+
return false;
282+
283+
string segment = segments[i].Trim();
284+
if (string.IsNullOrEmpty(segment))
285+
return false;
286+
287+
PropertyInfo nextProperty = currentType
288+
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
289+
.FirstOrDefault(p => p.GetIndexParameters().Length == 0 && string.Equals(p.Name, segment, StringComparison.OrdinalIgnoreCase));
290+
291+
if (nextProperty == null)
292+
return false;
293+
294+
object nextValue = currentValue == null ? null : nextProperty.GetValue(currentValue, null);
295+
if (i == segments.Length - 1)
296+
{
297+
result = new ExtractedObjProperty
298+
{
299+
Name = fullPath,
300+
Type = nextProperty.PropertyType,
301+
OriginalValue = nextValue
302+
};
303+
return true;
304+
}
305+
306+
currentType = nextProperty.PropertyType;
307+
currentValue = nextValue;
308+
}
309+
310+
return false;
311+
}
239312
}
240-
}
313+
}

0 commit comments

Comments
 (0)