Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions ObjectSemantics.NET.Tests/CognitiveMapTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,33 @@ public void Should_Escape_Xml_Char_Values_If_Option_Is_Enabled(string value, str
});
Assert.Equal(expected, generatedTemplate);
}


[Fact]
public void Additional_Headers_And_Class_Properties_Should_Also_Be_Mapped_Combined()
{
Payment payment = new Payment
{
Id = 1,
Amount = 1000,
PayMethod = "CHEQUE",
PayMethodId = 2,
ReferenceNo = "CHEQUE0001",
UserId = 242
};
//additional params (outside the class)
Dictionary<string, object> additionalParams = new Dictionary<string, object>
{
{ "ReceivedBy", "John Doe"},
{ "NewBalance", 1050 }
};

string generatedTemplate = payment.Map("{{Id}}-{{ ReferenceNo }} Confirmed. ${{ Amount:N2 }} received via {{ PayMethod }}({{PayMethodId}}) from user-id {{ UserId }}. New Balance: {{ NewBalance:N2 }}, Received By: {{ReceivedBy}}.", additionalParams);


string expectedResponse = "1-CHEQUE0001 Confirmed. $1,000.00 received via CHEQUE(2) from user-id 242. New Balance: 1,050.00, Received By: John Doe.";

Assert.Equal(generatedTemplate, expectedResponse);
}
}
}
15 changes: 15 additions & 0 deletions ObjectSemantics.NET.Tests/EnumerableLoopTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,5 +140,20 @@ public void Should_Map_Array_Of_String_With_Formatting()
string expectedResult = " MORGAN GEORGE JANE ";
Assert.Equal(expectedResult, generatedTemplate, false, true, true);
}

[Theory]
[InlineData(@"{{ #foreach(MyFriends) }}[{{ .:uppercase }}]{{ #endforeach }}")]
[InlineData(@"{{# foreach(MyFriends) }}[{{ .:uppercase }}]{{# endforeach }}")]
[InlineData(@"{{ #foreach(MyFriends) }}[{{ .:uppercase }}]{{ #endforeach }}")]
public void Should_Evaluate_ForEach_Having_Spaces_Before_And_After_Parentheses(string template)
{
Person person = new Person
{
MyFriends = new string[] { "Morgan", "George" }
};

string result = person.Map(template);
Assert.Equal("[MORGAN][GEORGE]", result);
}
}
}
16 changes: 16 additions & 0 deletions ObjectSemantics.NET.Tests/IfConditionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,21 @@ public void Should_Render_Multiple_If_Condition_Statements(int age, string expec
string result = person.Map(template);
Assert.Equal(expected, result);
}


[Theory]
[InlineData(@"{{#if(MyCars==0)}}Zero Cars{{#else}}Hmmm!{{#endif}}")]
[InlineData(@"{{# if (MyCars==0)}}Zero Cars{{ # else }}Hmmm!{{ # endif}}")]
[InlineData(@"{{ #if(MyCars==0) }}Zero Cars{{ #endif }}")]
public void Should_Evaluate_If_Having_Spaces_Before_And_After_Parentheses(string template)
{
Person person = new Person
{
MyCars = null
};

string result = person.Map(template);
Assert.Equal("Zero Cars", result);
}
}
}
52 changes: 52 additions & 0 deletions ObjectSemantics.NET.Tests/MoqFiles/PaymentTemplate.result.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<Template font="Calibri" FontSize="10" MarginTop="10">

<Data FontStyle="Bold" FontSize="11" TextWrap ="True" Align="Center">TEST COMPANY</Data>
<Data TextWrap ="True" Align="Center">test@gmail.com</Data>
<Data TextWrap ="True" Align="Center">Test Address</Data>
<Data TextWrap ="True" Align="Center">+2547000000001</Data>

<LineBreak/>
<Data FontStyle="Bold" FontSize="12" Align="Center">CUSTOMER PAYMENT RECEIPT</Data>

<LineBreak/>

<Data>Payment No #: 12719</Data>
<Data>Ref No #: CP-20251029-14QH</Data>
<Data>Payment Date: 29-Oct-2025 02:03 PM</Data>

<Line Style="Dash" />
<Data FontStyle="Bold" Align="Right">PAYMENT TO</Data>
<Data TextWrap ="True" Align="Right">MAIN BRANCH</Data>
<Data TextWrap ="True" Align="Right">Account: Cash A/C</Data>
<Data TextWrap ="True" Align="Right">Account Code: 1</Data>
<LineBreak/>

<Line Style="Dash" />
<Data FontStyle="Bold">PAYMENT FROM</Data>
<Data TextWrap ="True">JOHN DOE ENTERPRISES</Data>
<Data TextWrap ="True">Account Code: 54</Data>
<Data TextWrap ="True">Paid By: JOHN DOE</Data>
<LineBreak/>

<Line Style="Dash" />

<Grid ColumnWidths="2*1">
<GridRow>
<Data Grid.Column="0" FontStyle="Bold">AMOUNT</Data>
<Data Grid.Column="1" FontStyle="Bold" FontSize="11" Align="Right">300.00</Data>
</GridRow>
</Grid>

<Data Align="Center" TextWrap="True"></Data>
<Line Style="Dash" />

<LineBreak/>

<Data>Payment received by: George Waynne</Data>
<LineBreak/>
<Data Align="Center" TextWrap ="True">Customer Prev. Bal: 19,395.00, Current Bal: 19,095.00</Data>
<LineBreak/>
<LineBreak/>
<Data Align="Center" FontSize="8">Powered By Semantic POS | www.semanticpos.com</Data>

</Template>
52 changes: 52 additions & 0 deletions ObjectSemantics.NET.Tests/MoqFiles/PaymentTemplate.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<Template font="Calibri" FontSize="10" MarginTop="10">

<Data FontStyle="Bold" FontSize="11" TextWrap ="True" Align="Center">{{ CompanyName }}</Data>
<Data TextWrap ="True" Align="Center">{{ CompanyEmail }}</Data>
<Data TextWrap ="True" Align="Center">{{ CompanyAddress }}</Data>
<Data TextWrap ="True" Align="Center">{{ CompanyMobile }}</Data>

<LineBreak/>
<Data FontStyle="Bold" FontSize="12" Align="Center">CUSTOMER PAYMENT RECEIPT</Data>

<LineBreak/>

<Data>Payment No #: {{ Id }}</Data>
<Data>Ref No #: {{ ReferenceNo }}</Data>
<Data>Payment Date: {{ PaymentDate:dd-MMM-yyyy hh:mm tt }}</Data>

<Line Style="Dash" />
<Data FontStyle="Bold" Align="Right">PAYMENT TO</Data>
<Data TextWrap ="True" Align="Right">{{ BranchName }}</Data>
<Data TextWrap ="True" Align="Right">Account: {{ LedgerAccountName }}</Data>
<Data TextWrap ="True" Align="Right">Account Code: {{ LedgerAccountId }}</Data>
<LineBreak/>

<Line Style="Dash" />
<Data FontStyle="Bold">PAYMENT FROM</Data>
<Data TextWrap ="True">{{ CustomerName }}</Data>
<Data TextWrap ="True">Account Code: {{ CustomerId }}</Data>
<Data TextWrap ="True">Paid By: {{ PaidBy }}</Data>
<LineBreak/>

<Line Style="Dash" />

<Grid ColumnWidths="2*1">
<GridRow>
<Data Grid.Column="0" FontStyle="Bold">AMOUNT</Data>
<Data Grid.Column="1" FontStyle="Bold" FontSize="11" Align="Right">{{ Amount:N2 }}</Data>
</GridRow>
</Grid>

<Data Align="Center" TextWrap="True">{{ Narration }}</Data>
<Line Style="Dash" />

<LineBreak/>

<Data>Payment received by: {{ RegisteredBy }}</Data>
<LineBreak/>
<Data Align="Center" TextWrap ="True">Customer Prev. Bal: {{ customer_prevBalance }}, Current Bal: {{ customer_currentBalance }}</Data>
<LineBreak/>
<LineBreak/>
<Data Align="Center" FontSize="8">Powered By Semantic POS | www.semanticpos.com</Data>

</Template>
28 changes: 28 additions & 0 deletions ObjectSemantics.NET.Tests/MoqModels/CustomerPayment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;

namespace ObjectSemantics.NET.Tests.MoqModels
{
public class CustomerPayment
{
public int Id { get; set; }
public int CustomerId { get; set; }
public double Amount { get; set; }
public string ReferenceNo { get; set; }
public string PaidBy { get; set; }
public string RegisteredBy { get; set; }
public string Narration { get; set; }
public string CustomerName { get; set; }
public string LedgerAccountName { get; set; }
public int LedgerAccountId { get; set; }
public DateTime PaymentDate { get; set; }
public Customer Customer { get; set; }
}

public class Customer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string CompanyName { get; set; }
}
}
12 changes: 12 additions & 0 deletions ObjectSemantics.NET.Tests/MoqModels/Payment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace ObjectSemantics.NET.Tests.MoqModels
{
internal class Payment
{
public int Id { get; set; }
public int? UserId { get; set; }
public double Amount { get; set; } = 0;
public string PayMethod { get; set; }
public int? PayMethodId { get; set; }
public string ReferenceNo { get; set; }
}
}
14 changes: 14 additions & 0 deletions ObjectSemantics.NET.Tests/ObjectSemantics.NET.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@
<FileVersion>3.0.1.1</FileVersion>
</PropertyGroup>

<ItemGroup>
<None Remove="MoqFiles\PaymentTemplate.result.xml" />
<None Remove="MoqFiles\PaymentTemplate.xml" />
</ItemGroup>

<ItemGroup>
<Content Include="MoqFiles\PaymentTemplate.result.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="MoqFiles\PaymentTemplate.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
Expand Down
61 changes: 61 additions & 0 deletions ObjectSemantics.NET.Tests/TemplateFileMappingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using ObjectSemantics.NET.Tests.MoqModels;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Xunit;

namespace ObjectSemantics.NET.Tests
{
public class TemplateFileMappingTests
{
[Fact]
public void Should_Map_Large_File_Template()
{
string template = File.ReadAllText("MoqFiles/PaymentTemplate.xml", Encoding.UTF8);
string expectedResult = File.ReadAllText("MoqFiles/PaymentTemplate.result.xml", Encoding.UTF8);


var payment = new CustomerPayment
{
Id = 12719,
CustomerId = 54,
Amount = 300.0,
LedgerAccountId = 1,
ReferenceNo = "CP-20251029-14QH",
PaidBy = "JOHN DOE",
PaymentDate = DateTime.Parse("2025-10-29T14:03:19.4147588"),
RegisteredBy = "George Waynne",
Customer = new Customer
{
Id = 54,
FirstName = "JOHN DOE",
LastName = "ENTERPRISES",
CompanyName = "John Doe Enterprises",
},
Narration = null,
CustomerName = "JOHN DOE ENTERPRISES",
LedgerAccountName = "Cash A/C"
};

//additional headers
var additionalParams = new Dictionary<string, object>
{
["BranchName"] = "MAIN BRANCH",
["CompanyName"] = "TEST COMPANY",
["CompanyEmail"] = "test@gmail.com",
["CompanyAddress"] = "Test Address",
["CompanyMobile"] = "+2547000000001",
["customer_prevBalance"] = "19,395.00",
["customer_currentBalance"] = "19,095.00",
["CompanyLogo"] = "logo.jpg",
};

//map
string result = payment.Map(template, additionalParams);

Assert.Equal(result, expectedResult);
}

}
}
25 changes: 12 additions & 13 deletions ObjectSemantics.NET/Engine/EngineAlgorithim.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@ internal static class EngineAlgorithim
{
private static readonly ConcurrentDictionary<Type, PropertyInfo[]> PropertyCache = new ConcurrentDictionary<Type, PropertyInfo[]>();

private static readonly Regex IfConditionRegex = new Regex(@"{{\s*#if\s*\(\s*(?<param>\w+)\s*(?<operator>==|!=|>=|<=|>|<)\s*(?<value>[^)]+)\s*\)\s*}}(?<code>[\s\S]*?)(?:{{\s*#else\s*}}(?<else>[\s\S]*?))?{{\s*#endif\s*}}", RegexOptions.IgnoreCase | RegexOptions.Compiled);

private static readonly Regex LoopBlockRegex = new Regex(@"{{\s*#foreach\s*\(\s*(\w+)\s*\)\s*\}\}([\s\S]*?)\{\{\s*#endforeach\s*}}", RegexOptions.IgnoreCase | RegexOptions.Compiled);

private static readonly Regex IfConditionRegex = new Regex(@"{{\s*#\s*if\s*\(\s*(?<param>\w+)\s*(?<operator>==|!=|>=|<=|>|<)\s*(?<value>[^)]+?)\s*\)\s*}}(?<code>[\s\S]*?)(?:{{\s*#\s*else\s*}}(?<else>[\s\S]*?))?{{\s*#\s*endif\s*}}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex LoopBlockRegex = new Regex(@"{{\s*#\s*foreach\s*\(\s*(\w+)\s*\)\s*\}\}([\s\S]*?)\{\{\s*#\s*endforeach\s*}}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex DirectParamRegex = new Regex(@"{{(.+?)}}", RegexOptions.IgnoreCase | RegexOptions.Compiled);

public static string GenerateFromTemplate<T>(T record, EngineRunnerTemplate template, Dictionary<string, object> parameterKeyValues = null, TemplateMapperOptions options = null) where T : new()
Expand All @@ -33,7 +31,7 @@ internal static class EngineAlgorithim
{
if (!propMap.TryGetValue(ifCondition.IfPropertyName, out ExtractedObjProperty property))
{
result.Replace(ifCondition.ReplaceRef, "[IF-CONDITION EXCEPTION]: unrecognized property: [" + ifCondition.IfPropertyName + "]");
result.ReplaceFirstOccurrence(ifCondition.ReplaceRef, "[IF-CONDITION EXCEPTION]: unrecognized property: [" + ifCondition.IfPropertyName + "]");
continue;
}

Expand All @@ -55,15 +53,15 @@ internal static class EngineAlgorithim
replacement = string.Empty;
}

result.Replace(ifCondition.ReplaceRef, replacement);
result.ReplaceFirstOccurrence(ifCondition.ReplaceRef, replacement);
}

// ---- Object Loops ----
foreach (ReplaceObjLoopCode objLoop in template.ReplaceObjLoopCodes)
{
if (!propMap.TryGetValue(objLoop.TargetObjectName, out ExtractedObjProperty targetObj) || !(targetObj.OriginalValue is IEnumerable enumerable))
{
result.Replace(objLoop.ReplaceRef, string.Empty);
result.ReplaceFirstOccurrence(objLoop.ReplaceRef, string.Empty);
continue;
}

Expand All @@ -87,31 +85,32 @@ internal static class EngineAlgorithim
Type = row.GetType(),
OriginalValue = row
};
activeRow.Replace(objLoopCode.ReplaceRef, tempProp.GetPropertyDisplayString(objLoopCode.GetFormattingCommand(), options));
activeRow.ReplaceFirstOccurrence(objLoopCode.ReplaceRef, tempProp.GetPropertyDisplayString(objLoopCode.GetFormattingCommand(), options));
}
else
{
if (rowMap.TryGetValue(propName, out ExtractedObjProperty p))
activeRow.Replace(objLoopCode.ReplaceRef, p.GetPropertyDisplayString(objLoopCode.GetFormattingCommand(), options));
activeRow.ReplaceFirstOccurrence(objLoopCode.ReplaceRef, p.GetPropertyDisplayString(objLoopCode.GetFormattingCommand(), options));
else
activeRow.Replace(objLoopCode.ReplaceRef, objLoopCode.ReplaceCommand);
activeRow.ReplaceFirstOccurrence(objLoopCode.ReplaceRef, objLoopCode.ReplaceCommand);
}
}

loopResult.Append(activeRow);
}

result.Replace(objLoop.ReplaceRef, loopResult.ToString());
result.ReplaceFirstOccurrence(objLoop.ReplaceRef, loopResult.ToString());
}

// ---- Direct Replacements ----
foreach (ReplaceCode replaceCode in template.ReplaceCodes)
{
if (propMap.TryGetValue(replaceCode.GetTargetPropertyName(), out ExtractedObjProperty property))
result.Replace(replaceCode.ReplaceRef, property.GetPropertyDisplayString(replaceCode.GetFormattingCommand(), options));
result.ReplaceFirstOccurrence(replaceCode.ReplaceRef, property.GetPropertyDisplayString(replaceCode.GetFormattingCommand(), options));
else
result.Replace(replaceCode.ReplaceRef, "{{ " + replaceCode.ReplaceCommand + " }}");
result.ReplaceFirstOccurrence(replaceCode.ReplaceRef, "{{ " + replaceCode.ReplaceCommand + " }}");
}

return result.ToString();
}

Expand Down
Loading