diff --git a/ObjectSemantics.NET.Tests/CognitiveMapTests.cs b/ObjectSemantics.NET.Tests/CognitiveMapTests.cs index f909a0d..5dea7e9 100644 --- a/ObjectSemantics.NET.Tests/CognitiveMapTests.cs +++ b/ObjectSemantics.NET.Tests/CognitiveMapTests.cs @@ -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 additionalParams = new Dictionary + { + { "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); + } } } diff --git a/ObjectSemantics.NET.Tests/EnumerableLoopTests.cs b/ObjectSemantics.NET.Tests/EnumerableLoopTests.cs index e7702e3..1862556 100644 --- a/ObjectSemantics.NET.Tests/EnumerableLoopTests.cs +++ b/ObjectSemantics.NET.Tests/EnumerableLoopTests.cs @@ -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); + } } } diff --git a/ObjectSemantics.NET.Tests/IfConditionTests.cs b/ObjectSemantics.NET.Tests/IfConditionTests.cs index 26666e8..2111ff7 100644 --- a/ObjectSemantics.NET.Tests/IfConditionTests.cs +++ b/ObjectSemantics.NET.Tests/IfConditionTests.cs @@ -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); + } } } diff --git a/ObjectSemantics.NET.Tests/MoqFiles/PaymentTemplate.result.xml b/ObjectSemantics.NET.Tests/MoqFiles/PaymentTemplate.result.xml new file mode 100644 index 0000000..65ca76d --- /dev/null +++ b/ObjectSemantics.NET.Tests/MoqFiles/PaymentTemplate.result.xml @@ -0,0 +1,52 @@ + \ No newline at end of file diff --git a/ObjectSemantics.NET.Tests/MoqFiles/PaymentTemplate.xml b/ObjectSemantics.NET.Tests/MoqFiles/PaymentTemplate.xml new file mode 100644 index 0000000..e7c6514 --- /dev/null +++ b/ObjectSemantics.NET.Tests/MoqFiles/PaymentTemplate.xml @@ -0,0 +1,52 @@ + \ No newline at end of file diff --git a/ObjectSemantics.NET.Tests/MoqModels/CustomerPayment.cs b/ObjectSemantics.NET.Tests/MoqModels/CustomerPayment.cs new file mode 100644 index 0000000..1fbf860 --- /dev/null +++ b/ObjectSemantics.NET.Tests/MoqModels/CustomerPayment.cs @@ -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; } + } +} diff --git a/ObjectSemantics.NET.Tests/MoqModels/Payment.cs b/ObjectSemantics.NET.Tests/MoqModels/Payment.cs new file mode 100644 index 0000000..2e69d17 --- /dev/null +++ b/ObjectSemantics.NET.Tests/MoqModels/Payment.cs @@ -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; } + } +} diff --git a/ObjectSemantics.NET.Tests/ObjectSemantics.NET.Tests.csproj b/ObjectSemantics.NET.Tests/ObjectSemantics.NET.Tests.csproj index c804413..5e4177c 100644 --- a/ObjectSemantics.NET.Tests/ObjectSemantics.NET.Tests.csproj +++ b/ObjectSemantics.NET.Tests/ObjectSemantics.NET.Tests.csproj @@ -10,6 +10,20 @@ 3.0.1.1 + + + + + + + + Always + + + Always + + + diff --git a/ObjectSemantics.NET.Tests/TemplateFileMappingTests.cs b/ObjectSemantics.NET.Tests/TemplateFileMappingTests.cs new file mode 100644 index 0000000..5a392ca --- /dev/null +++ b/ObjectSemantics.NET.Tests/TemplateFileMappingTests.cs @@ -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 + { + ["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); + } + + } +} diff --git a/ObjectSemantics.NET/Engine/EngineAlgorithim.cs b/ObjectSemantics.NET/Engine/EngineAlgorithim.cs index 0b75f4e..c3bce8a 100644 --- a/ObjectSemantics.NET/Engine/EngineAlgorithim.cs +++ b/ObjectSemantics.NET/Engine/EngineAlgorithim.cs @@ -15,10 +15,8 @@ internal static class EngineAlgorithim { private static readonly ConcurrentDictionary PropertyCache = new ConcurrentDictionary(); - private static readonly Regex IfConditionRegex = new Regex(@"{{\s*#if\s*\(\s*(?\w+)\s*(?==|!=|>=|<=|>|<)\s*(?[^)]+)\s*\)\s*}}(?[\s\S]*?)(?:{{\s*#else\s*}}(?[\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*(?\w+)\s*(?==|!=|>=|<=|>|<)\s*(?[^)]+?)\s*\)\s*}}(?[\s\S]*?)(?:{{\s*#\s*else\s*}}(?[\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 record, EngineRunnerTemplate template, Dictionary parameterKeyValues = null, TemplateMapperOptions options = null) where T : new() @@ -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; } @@ -55,7 +53,7 @@ internal static class EngineAlgorithim replacement = string.Empty; } - result.Replace(ifCondition.ReplaceRef, replacement); + result.ReplaceFirstOccurrence(ifCondition.ReplaceRef, replacement); } // ---- Object Loops ---- @@ -63,7 +61,7 @@ internal static class EngineAlgorithim { 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; } @@ -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(); } diff --git a/ObjectSemantics.NET/Engine/Extensions/StringBuilderExtensions.cs b/ObjectSemantics.NET/Engine/Extensions/StringBuilderExtensions.cs new file mode 100644 index 0000000..9d500d2 --- /dev/null +++ b/ObjectSemantics.NET/Engine/Extensions/StringBuilderExtensions.cs @@ -0,0 +1,36 @@ +using System.Text; + +namespace ObjectSemantics.NET.Engine.Extensions +{ + public static class StringBuilderExtensions + { + public static StringBuilder ReplaceFirstOccurrence(this StringBuilder sb, string search, string replace) + { + if (sb == null || string.IsNullOrEmpty(search)) + return sb; + + int len = sb.Length - search.Length + 1; + for (int i = 0; i < len; i++) + { + bool match = true; + for (int j = 0; j < search.Length; j++) + { + if (sb[i + j] != search[j]) + { + match = false; + break; + } + } + + if (match) + { + sb.Remove(i, search.Length); + sb.Insert(i, replace); + break; + } + } + + return sb; + } + } +} diff --git a/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs b/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs index a0d49eb..1014a12 100644 --- a/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs +++ b/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs @@ -5,26 +5,6 @@ namespace ObjectSemantics.NET.Engine.Extensions { internal static class StringExtensions { - public static string ReplaceFirstOccurrence(this string text, string search, string replace) - { - int pos = text.IndexOf(search); - if (pos < 0) - return text; - return string.Format("{0}{1}{2}", text.Substring(0, pos), replace, text.Substring(pos + search.Length)); - } - public static string RemoveLastInstanceOfString(this string value, string removeString) - { - int index = value.LastIndexOf(removeString, StringComparison.Ordinal); - return index < 0 ? value : value.Remove(index, removeString.Length); - } - - public static string RemoveLastInstanceOfString(this string value, params char[] chars) - { - foreach (char c in chars) - value = value.RemoveLastInstanceOfString(c.ToString()); - return value; - } - public static string ToMD5String(this string input) { try diff --git a/ObjectSemantics.NET/ObjectSemantics.NET.csproj b/ObjectSemantics.NET/ObjectSemantics.NET.csproj index 31642f6..3dd06c1 100644 --- a/ObjectSemantics.NET/ObjectSemantics.NET.csproj +++ b/ObjectSemantics.NET/ObjectSemantics.NET.csproj @@ -15,9 +15,9 @@ ToBase64 FromBase64 . Added template extension method to allow mapping directly from Template - 7.0.0 - 7.0.0 - 7.0.0 + 7.0.1 + 7.0.1 + 7.0.1 false README.md