From 3d82835d8f5b11b28a51a2555e6c8611ba066565 Mon Sep 17 00:00:00 2001 From: "George Njeri (Swagfin)" Date: Mon, 6 Oct 2025 21:06:03 +0300 Subject: [PATCH 01/12] chore: base optimization and refactoring --- .../Algorithim/ExtractedObjProperty.cs | 18 -- .../Algorithim/GavinsAlgorithim.cs | 241 ----------------- ObjectSemantics.NET/Algorithim/ReplaceCode.cs | 26 -- .../Algorithim/ReplaceIfOperationCode.cs | 10 - .../Algorithim/ReplaceObjLoopCode.cs | 9 - .../Algorithim/TemplatedContent.cs | 9 - .../Engine/EngineAlgorithim.cs | 244 ++++++++++++++++++ .../ExtractedObjPropertyExtensions.cs | 172 ++++++++++++ .../Extensions/ReplaceCodeExtensions.cs | 36 +++ .../Extensions/StringExtensions.cs | 3 +- .../Engine/Models/EngineRunnerTemplate.cs | 12 + .../Engine/Models/ExtractedObjProperty.cs | 21 ++ .../Engine/Models/ReplaceCode.cs | 18 ++ .../Engine/Models/ReplaceIfOperationCode.cs | 12 + .../Engine/Models/ReplaceObjLoopCode.cs | 12 + .../ExtractedObjPropertyExtensions.cs | 138 ---------- .../ObjectSemantics.NET.csproj | 8 +- .../ObjectSemanticsKeyValue.cs | 12 +- ObjectSemantics.NET/TemplateMapper.cs | 10 +- 19 files changed, 543 insertions(+), 468 deletions(-) delete mode 100644 ObjectSemantics.NET/Algorithim/ExtractedObjProperty.cs delete mode 100644 ObjectSemantics.NET/Algorithim/GavinsAlgorithim.cs delete mode 100644 ObjectSemantics.NET/Algorithim/ReplaceCode.cs delete mode 100644 ObjectSemantics.NET/Algorithim/ReplaceIfOperationCode.cs delete mode 100644 ObjectSemantics.NET/Algorithim/ReplaceObjLoopCode.cs delete mode 100644 ObjectSemantics.NET/Algorithim/TemplatedContent.cs create mode 100644 ObjectSemantics.NET/Engine/EngineAlgorithim.cs create mode 100644 ObjectSemantics.NET/Engine/Extensions/ExtractedObjPropertyExtensions.cs create mode 100644 ObjectSemantics.NET/Engine/Extensions/ReplaceCodeExtensions.cs rename ObjectSemantics.NET/{ => Engine}/Extensions/StringExtensions.cs (98%) create mode 100644 ObjectSemantics.NET/Engine/Models/EngineRunnerTemplate.cs create mode 100644 ObjectSemantics.NET/Engine/Models/ExtractedObjProperty.cs create mode 100644 ObjectSemantics.NET/Engine/Models/ReplaceCode.cs create mode 100644 ObjectSemantics.NET/Engine/Models/ReplaceIfOperationCode.cs create mode 100644 ObjectSemantics.NET/Engine/Models/ReplaceObjLoopCode.cs delete mode 100644 ObjectSemantics.NET/Extensions/ExtractedObjPropertyExtensions.cs diff --git a/ObjectSemantics.NET/Algorithim/ExtractedObjProperty.cs b/ObjectSemantics.NET/Algorithim/ExtractedObjProperty.cs deleted file mode 100644 index b8f48b1..0000000 --- a/ObjectSemantics.NET/Algorithim/ExtractedObjProperty.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections; - -public class ExtractedObjProperty -{ - public Type Type { get; set; } - public string Name { get; set; } - public object OriginalValue { get; set; } - public string StringFormatted { get { return string.Format("{0}", OriginalValue); } } - public bool IsEnumerableObject - { - get { return typeof(IEnumerable).IsAssignableFrom(Type) && Type != typeof(string); } - } - public bool IsClassObject - { - get { return Type.IsClass && Type != typeof(string); } - } -} \ No newline at end of file diff --git a/ObjectSemantics.NET/Algorithim/GavinsAlgorithim.cs b/ObjectSemantics.NET/Algorithim/GavinsAlgorithim.cs deleted file mode 100644 index e985311..0000000 --- a/ObjectSemantics.NET/Algorithim/GavinsAlgorithim.cs +++ /dev/null @@ -1,241 +0,0 @@ -using ObjectSemantics.NET; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; - -public static class GavinsAlgorithim -{ - - public static string GenerateFromTemplate(T record, TemplatedContent clonedTemplate, List parameterKeyValues = null, TemplateMapperOptions options = null) where T : new() - { - //Get Object's Properties - List objProperties = GetObjectProperties(record, parameterKeyValues); - - #region Replace If Conditions - foreach (ReplaceIfOperationCode ifCondition in clonedTemplate.ReplaceIfConditionCodes) - { - ExtractedObjProperty property = objProperties.FirstOrDefault(x => x.Name.ToUpper().Equals(ifCondition.IfPropertyName.ToUpper())); - if (property != null) - { - if (property.IsPropertyValueConditionPassed(ifCondition.IfOperationValue, ifCondition.IfOperationType)) - { - //Condition Passed - TemplatedContent templatedIfContent = GenerateTemplateFromFileContents(ifCondition.IfOperationTrueTemplate, options); - string templatedIfContentMapped = GenerateFromTemplate(record, templatedIfContent, parameterKeyValues, options); - clonedTemplate.Template = clonedTemplate.Template.ReplaceFirstOccurrence(ifCondition.ReplaceRef, templatedIfContentMapped); - } - else if (!string.IsNullOrEmpty(ifCondition.IfOperationFalseTemplate)) - { - //If Else Condition Block - TemplatedContent templatedIfContent = GenerateTemplateFromFileContents(ifCondition.IfOperationFalseTemplate, options); - string templatedIfElseContentMapped = GenerateFromTemplate(record, templatedIfContent, parameterKeyValues, options); - clonedTemplate.Template = clonedTemplate.Template.ReplaceFirstOccurrence(ifCondition.ReplaceRef, templatedIfElseContentMapped); - } - else - clonedTemplate.Template = clonedTemplate.Template.ReplaceFirstOccurrence(ifCondition.ReplaceRef, string.Empty); - } - else - clonedTemplate.Template = clonedTemplate.Template.ReplaceFirstOccurrence(ifCondition.ReplaceRef, $"[IF-CONDITION EXCEPTION]: unrecognized property: [{ifCondition.IfPropertyName}]"); - } - #endregion - - #region Replace Obj Loop Attributes - foreach (ReplaceObjLoopCode objLoop in clonedTemplate.ReplaceObjLoopCodes) - { - //First Loop lets look at its target property Object - ExtractedObjProperty targetObj = objProperties.Where(x => x.IsEnumerableObject).FirstOrDefault(x => x.Name.ToUpper().Equals(objLoop.TargetObjectName.ToUpper())); - //Since this Object is of Type Enumerable - if (targetObj != null && targetObj.OriginalValue != null) - { - //Loop all data records inside target Property - string rowTemplate = objLoop.ObjLoopTemplate; - StringBuilder rowContentTemplater = new StringBuilder(); - foreach (object loopRow in (IEnumerable)targetObj.OriginalValue) - { - List rowRecordValues = GetObjPropertiesFromUnknown(loopRow); - //Loop Through class template Loops - string activeRow = rowTemplate; - foreach (ReplaceCode objLoopCode in objLoop.ReplaceObjCodes) - { - ExtractedObjProperty objProperty = rowRecordValues.FirstOrDefault(x => x.Name.ToUpper().Equals(objLoopCode.TargetPropertyName.ToUpper())); - if (objProperty != null) - activeRow = activeRow.ReplaceFirstOccurrence(objLoopCode.ReplaceRef, objProperty.GetPropertyDisplayString(objLoopCode.FormattingCommand, options)); - else if (objLoopCode.TargetPropertyName.Equals(".")) - { - activeRow = activeRow.ReplaceFirstOccurrence(objLoopCode.ReplaceRef, new ExtractedObjProperty - { - Name = objLoopCode.TargetPropertyName, - Type = loopRow.GetType(), - OriginalValue = loopRow - }.GetPropertyDisplayString(objLoopCode.FormattingCommand, options)); - } - else - activeRow = activeRow.ReplaceFirstOccurrence(objLoopCode.ReplaceRef, objLoopCode.ReplaceCommand); - } - //Append Record row - rowContentTemplater.Append(activeRow); - } - objLoop.ObjLoopTemplate = rowContentTemplater.ToString().RemoveLastInstanceOfString('\r', '\n'); //Assign Auto Generated - //Replace the main Loop area - clonedTemplate.Template = clonedTemplate.Template.ReplaceFirstOccurrence(objLoop.ReplaceRef, objLoop.ObjLoopTemplate); - } - else - clonedTemplate.Template = clonedTemplate.Template.ReplaceFirstOccurrence(objLoop.ReplaceRef, string.Empty); - - } - #endregion - - #region Replace Direct Target Attributes - foreach (ReplaceCode replaceCode in clonedTemplate.ReplaceCodes) - { - ExtractedObjProperty property = objProperties.FirstOrDefault(x => x.Name.ToUpper().Equals(replaceCode.TargetPropertyName.ToUpper())); - if (property != null) - clonedTemplate.Template = clonedTemplate.Template.ReplaceFirstOccurrence(replaceCode.ReplaceRef, property.GetPropertyDisplayString(replaceCode.FormattingCommand, options)); - else - clonedTemplate.Template = clonedTemplate.Template.ReplaceFirstOccurrence(replaceCode.ReplaceRef, @"{{ ##command## }}".Replace("##command##", replaceCode.ReplaceCommand)); - } - #endregion - return clonedTemplate.Template; - } - - internal static TemplatedContent GenerateTemplateFromFileContents(string fileContent, TemplateMapperOptions options) - { - TemplatedContent templatedContent = new TemplatedContent { Template = fileContent }; - long _replaceKey = 0; - #region If Condition - //Matches ==, !=, >, >=, <, and <= - string _matchWithElseIf = @"{{\s*#if\s*\(\s*(?\w+)\s*(?==|!=|>=|<=|>|<)\s*(?[^)]+)\s*\)\s*}}(?[\s\S]*?)(?:{{\s*#else\s*}}(?[\s\S]*?))?{{\s*#endif\s*}}"; - while (Regex.IsMatch(templatedContent.Template, _matchWithElseIf, RegexOptions.IgnoreCase)) - { - templatedContent.Template = Regex.Replace(templatedContent.Template, _matchWithElseIf, match => - { - _replaceKey++; - string _replaceCode = string.Format("RIB_{0}", _replaceKey); - templatedContent.ReplaceIfConditionCodes.Add(new ReplaceIfOperationCode - { - ReplaceRef = _replaceCode, - IfPropertyName = match.Groups[1].Value?.ToString().Trim().ToLower().Replace(" ", string.Empty), - IfOperationType = match.Groups[2].Value?.ToString().Trim().ToLower().Replace(" ", string.Empty), - IfOperationValue = match.Groups[3].Value?.ToString().Trim(), - IfOperationTrueTemplate = match.Groups[4].Value?.ToString(), - IfOperationFalseTemplate = (match.Groups.Count >= 6) ? match.Groups[5].Value?.ToString() : string.Empty - }); - // Return Replacement - return _replaceCode; - }, RegexOptions.IgnoreCase); - } - #endregion - - #region Generate Obj Looop - string _matchLoopBlock = @"{{\s*#\s*foreach\s*\(\s*(\w+)\s*\)\s*\}\}([\s\S]*?)\{\{\s*#\s*endforeach\s*}}"; - while (Regex.IsMatch(templatedContent.Template, _matchLoopBlock, RegexOptions.IgnoreCase)) - { - templatedContent.Template = Regex.Replace(templatedContent.Template, _matchLoopBlock, match => - { - _replaceKey++; - string _replaceCode = string.Format("RLB_{0}", _replaceKey); - ReplaceObjLoopCode objLoopCode = new ReplaceObjLoopCode - { - ReplaceRef = _replaceCode, - TargetObjectName = match.Groups[1].Value?.ToString().Trim().ToLower().Replace(" ", string.Empty) - }; - //Determine Forloop Elements - string loopBlock = match.Groups[2].Value?.ToString() ?? string.Empty; - string loopBlockRegex = @"{{(.+?)}}"; - while (Regex.IsMatch(loopBlock, loopBlockRegex, RegexOptions.IgnoreCase)) - { - loopBlock = Regex.Replace(loopBlock, loopBlockRegex, loopBlockMatch => - { - _replaceKey++; - string _replaceLoopBlockCode = string.Format("RLBR_{0}", _replaceKey); - objLoopCode.ReplaceObjCodes.Add(new ReplaceCode - { - ReplaceCommand = loopBlockMatch.Groups[1].Value?.Trim(), - ReplaceRef = _replaceLoopBlockCode - }); - return _replaceLoopBlockCode; - }, RegexOptions.IgnoreCase); - } - //#Just before return - objLoopCode.ObjLoopTemplate = loopBlock; - templatedContent.ReplaceObjLoopCodes.Add(objLoopCode); - // Return Replacement - return _replaceCode; - }, RegexOptions.IgnoreCase); - } - - #endregion - - #region Generate direct targets - string _paramRegex = @"{{(.+?)}}"; - while (Regex.IsMatch(templatedContent.Template, _paramRegex, RegexOptions.IgnoreCase)) - { - templatedContent.Template = Regex.Replace(templatedContent.Template, _paramRegex, propertyMatch => - { - _replaceKey++; - string _replaceCode = string.Format("RP_{0}", _replaceKey); - templatedContent.ReplaceCodes.Add(new ReplaceCode - { - ReplaceCommand = propertyMatch.Groups[1].Value?.Trim(), - ReplaceRef = _replaceCode - }); - return _replaceCode; - }, RegexOptions.IgnoreCase); - } - - #endregion - - return templatedContent; - } - - - private static List GetObjectProperties(T value, List parameters = null) where T : new() - { - List extractedObjProperties = new List(); - foreach (PropertyInfo prop in typeof(T).GetProperties()) - { - try - { - extractedObjProperties.Add(new ExtractedObjProperty - { - Type = prop.PropertyType, - Name = prop.Name, - OriginalValue = value == null ? null : prop.GetValue(value) - }); - } - catch { } - } - //Append Parameters - if (parameters != null && parameters.Count != 0) - foreach (var param in parameters) - extractedObjProperties.Add(new ExtractedObjProperty { Type = param.Type, Name = param.Key, OriginalValue = param.Value }); - return extractedObjProperties; - } - private static List GetObjPropertiesFromUnknown(object value) - { - List list = new List(); - if (value == null) - return list; - Type myType = value.GetType(); - IList props = new List(myType.GetProperties()); - //loop (Skip properties that require index parameters (like Chars in string)) - foreach (PropertyInfo prop in props.Where(x => x.GetIndexParameters().Length == 0)) - { - try - { - list.Add(new ExtractedObjProperty - { - Type = prop.PropertyType, - Name = prop.Name, - OriginalValue = value == null ? null : prop.GetValue(value) - }); - } - catch { } - } - return list; - } -} \ No newline at end of file diff --git a/ObjectSemantics.NET/Algorithim/ReplaceCode.cs b/ObjectSemantics.NET/Algorithim/ReplaceCode.cs deleted file mode 100644 index 5687fb4..0000000 --- a/ObjectSemantics.NET/Algorithim/ReplaceCode.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Linq; -using System.Text.RegularExpressions; - -public class ReplaceCode -{ - public string ReplaceRef { get; set; } - public string ReplaceCommand { get; set; } - public string TargetPropertyName - { - get - { - return ReplaceCommand?.Trim().Split(new string[] { ":" }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); - } - } - public string FormattingCommand - { - get - { - Match hasFormatting = Regex.Match(ReplaceCommand, "##command##:(.+)".Replace("##command##", TargetPropertyName), RegexOptions.IgnoreCase); - if (hasFormatting.Success) - return hasFormatting.Groups[1].Value; - return string.Empty; - } - } -} diff --git a/ObjectSemantics.NET/Algorithim/ReplaceIfOperationCode.cs b/ObjectSemantics.NET/Algorithim/ReplaceIfOperationCode.cs deleted file mode 100644 index ea567bb..0000000 --- a/ObjectSemantics.NET/Algorithim/ReplaceIfOperationCode.cs +++ /dev/null @@ -1,10 +0,0 @@ -public class ReplaceIfOperationCode -{ - public string IfPropertyName { get; set; } - public string IfOperationType { get; set; } - public string IfOperationValue { get; set; } - public string ReplaceRef { get; set; } - public string IfOperationTrueTemplate { get; set; } = string.Empty; - public string IfOperationFalseTemplate { get; set; } = string.Empty; -} - diff --git a/ObjectSemantics.NET/Algorithim/ReplaceObjLoopCode.cs b/ObjectSemantics.NET/Algorithim/ReplaceObjLoopCode.cs deleted file mode 100644 index 90b58f0..0000000 --- a/ObjectSemantics.NET/Algorithim/ReplaceObjLoopCode.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -public class ReplaceObjLoopCode -{ - public string ReplaceRef { get; set; } - public string TargetObjectName { get; set; } - public string ObjLoopTemplate { get; set; } - public List ReplaceObjCodes { get; set; } = new List(); -} diff --git a/ObjectSemantics.NET/Algorithim/TemplatedContent.cs b/ObjectSemantics.NET/Algorithim/TemplatedContent.cs deleted file mode 100644 index c016247..0000000 --- a/ObjectSemantics.NET/Algorithim/TemplatedContent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -public class TemplatedContent -{ - public string Template { get; set; } - public List ReplaceObjLoopCodes { get; set; } = new List(); - public List ReplaceCodes { get; set; } = new List(); - public List ReplaceIfConditionCodes { get; set; } = new List(); -} diff --git a/ObjectSemantics.NET/Engine/EngineAlgorithim.cs b/ObjectSemantics.NET/Engine/EngineAlgorithim.cs new file mode 100644 index 0000000..45840c6 --- /dev/null +++ b/ObjectSemantics.NET/Engine/EngineAlgorithim.cs @@ -0,0 +1,244 @@ +using ObjectSemantics.NET.Engine.Extensions; +using ObjectSemantics.NET.Engine.Models; +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; + +namespace ObjectSemantics.NET.Engine +{ + public 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 DirectParamRegex = new Regex(@"{{(.+?)}}", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + public static string GenerateFromTemplate(T record, EngineRunnerTemplate template, List parameterKeyValues = null, TemplateMapperOptions options = null) where T : new() + { + List objProperties = GetObjectProperties(record, parameterKeyValues); + Dictionary propMap = objProperties.ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase); + + StringBuilder result = new StringBuilder(template.Template ?? string.Empty, (template.Template?.Length ?? 0) * 2); + + // ---- IF Conditions ---- + foreach (ReplaceIfOperationCode ifCondition in template.ReplaceIfConditionCodes) + { + if (!propMap.TryGetValue(ifCondition.IfPropertyName, out ExtractedObjProperty property)) + { + result.Replace(ifCondition.ReplaceRef, "[IF-CONDITION EXCEPTION]: unrecognized property: [" + ifCondition.IfPropertyName + "]"); + continue; + } + + bool conditionPassed = property.IsPropertyValueConditionPassed(ifCondition.IfOperationValue, ifCondition.IfOperationType); + string replacement; + + if (conditionPassed) + { + EngineRunnerTemplate trueContent = GenerateRunnerTemplate(ifCondition.IfOperationTrueTemplate); + replacement = GenerateFromTemplate(record, trueContent, parameterKeyValues, options); + } + else if (!string.IsNullOrEmpty(ifCondition.IfOperationFalseTemplate)) + { + EngineRunnerTemplate falseContent = GenerateRunnerTemplate(ifCondition.IfOperationFalseTemplate); + replacement = GenerateFromTemplate(record, falseContent, parameterKeyValues, options); + } + else + { + replacement = string.Empty; + } + + result.Replace(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); + continue; + } + + StringBuilder loopResult = new StringBuilder(); + + foreach (object row in enumerable) + { + List rowProps = GetObjPropertiesFromUnknown(row); + Dictionary rowMap = rowProps.ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase); + + StringBuilder activeRow = new StringBuilder(objLoop.ObjLoopTemplate); + + foreach (ReplaceCode objLoopCode in objLoop.ReplaceObjCodes) + { + if (objLoopCode.TargetPropertyName == ".") + { + ExtractedObjProperty tempProp = new ExtractedObjProperty + { + Name = ".", + Type = row.GetType(), + OriginalValue = row + }; + activeRow.Replace(objLoopCode.ReplaceRef, + tempProp.GetPropertyDisplayString(objLoopCode.GetFormattingCommand(), options)); + } + else + { + if (rowMap.TryGetValue(objLoopCode.TargetPropertyName, out ExtractedObjProperty p)) + activeRow.Replace(objLoopCode.ReplaceRef, + p.GetPropertyDisplayString(objLoopCode.GetFormattingCommand(), options)); + else + activeRow.Replace(objLoopCode.ReplaceRef, objLoopCode.ReplaceCommand); + } + } + + loopResult.Append(activeRow); + } + + result.Replace(objLoop.ReplaceRef, loopResult.ToString()); + } + + // ---- Direct Replacements ---- + foreach (ReplaceCode replaceCode in template.ReplaceCodes) + { + if (propMap.TryGetValue(replaceCode.TargetPropertyName, out ExtractedObjProperty property)) + result.Replace(replaceCode.ReplaceRef, + property.GetPropertyDisplayString(replaceCode.GetFormattingCommand(), options)); + else + result.Replace(replaceCode.ReplaceRef, "{{ " + replaceCode.ReplaceCommand + " }}"); + } + + return result.ToString(); + } + + internal static EngineRunnerTemplate GenerateRunnerTemplate(string fileContent) + { + EngineRunnerTemplate templatedContent = new EngineRunnerTemplate { Template = fileContent }; + long key = 0; + + // ---- IF Conditions ---- + templatedContent.Template = IfConditionRegex.Replace(templatedContent.Template, m => + { + key++; + string refKey = "RIB_" + key; + templatedContent.ReplaceIfConditionCodes.Add(new ReplaceIfOperationCode + { + ReplaceRef = refKey, + IfPropertyName = m.Groups["param"].Value, + IfOperationType = m.Groups["operator"].Value, + IfOperationValue = m.Groups["value"].Value, + IfOperationTrueTemplate = m.Groups["code"].Value, + IfOperationFalseTemplate = m.Groups["else"].Success ? m.Groups["else"].Value : string.Empty + }); + return refKey; + }); + + // ---- FOREACH Loops ---- + templatedContent.Template = LoopBlockRegex.Replace(templatedContent.Template, m => + { + key++; + string refKey = "RLB_" + key; + ReplaceObjLoopCode objLoop = new ReplaceObjLoopCode + { + ReplaceRef = refKey, + TargetObjectName = m.Groups[1].Value + }; + + string loopBlock = m.Groups[2].Value; + loopBlock = DirectParamRegex.Replace(loopBlock, pm => + { + key++; + string loopRef = "RLBR_" + key; + objLoop.ReplaceObjCodes.Add(new ReplaceCode + { + ReplaceCommand = pm.Groups[1].Value.Trim(), + ReplaceRef = loopRef + }); + return loopRef; + }); + + objLoop.ObjLoopTemplate = loopBlock; + templatedContent.ReplaceObjLoopCodes.Add(objLoop); + return refKey; + }); + + // ---- Direct Parameters ---- + templatedContent.Template = DirectParamRegex.Replace(templatedContent.Template, m => + { + key++; + string refKey = "RP_" + key; + templatedContent.ReplaceCodes.Add(new ReplaceCode + { + ReplaceCommand = m.Groups[1].Value.Trim(), + ReplaceRef = refKey + }); + return refKey; + }); + + return templatedContent; + } + + private static List GetObjectProperties(T value, List parameters) where T : new() + { + Type type = typeof(T); + if (!PropertyCache.TryGetValue(type, out PropertyInfo[] props)) + { + props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + PropertyCache[type] = props; + } + + List result = new List(props.Length + (parameters != null ? parameters.Count : 0)); + + foreach (PropertyInfo prop in props) + { + result.Add(new ExtractedObjProperty + { + Type = prop.PropertyType, + Name = prop.Name, + OriginalValue = value == null ? null : prop.GetValue(value, null) + }); + } + + if (parameters != null) + { + foreach (ObjectSemanticsKeyValue p in parameters) + result.Add(new ExtractedObjProperty { Type = p.Value.GetType(), Name = p.Key, OriginalValue = p.Value }); + } + + return result; + } + + private static List GetObjPropertiesFromUnknown(object value) + { + if (value == null) return new List(); + + Type type = value.GetType(); + if (!PropertyCache.TryGetValue(type, out PropertyInfo[] props)) + { + props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.GetIndexParameters().Length == 0).ToArray(); + PropertyCache[type] = props; + } + + List result = new List(props.Length); + foreach (PropertyInfo prop in props) + { + result.Add(new ExtractedObjProperty + { + Type = prop.PropertyType, + Name = prop.Name, + OriginalValue = prop.GetValue(value, null) + }); + } + + return result; + } + } +} \ No newline at end of file diff --git a/ObjectSemantics.NET/Engine/Extensions/ExtractedObjPropertyExtensions.cs b/ObjectSemantics.NET/Engine/Extensions/ExtractedObjPropertyExtensions.cs new file mode 100644 index 0000000..c0b041e --- /dev/null +++ b/ObjectSemantics.NET/Engine/Extensions/ExtractedObjPropertyExtensions.cs @@ -0,0 +1,172 @@ +using ObjectSemantics.NET.Engine.Models; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Security; + +namespace ObjectSemantics.NET.Engine.Extensions +{ + public static class ExtractedObjPropertyExtensions + { + public static string GetPropertyDisplayString(this ExtractedObjProperty p, string stringFormatting, TemplateMapperOptions options) + { + if (p == null) + return string.Empty; + + string formatted = p.GetAppliedPropertyFormatting(stringFormatting); + if (options?.XmlCharEscaping == true && !string.IsNullOrEmpty(formatted)) + formatted = SecurityElement.Escape(formatted); + + return formatted; + } + + private static string GetAppliedPropertyFormatting(this ExtractedObjProperty p, string customFormat) + { + if (string.IsNullOrEmpty(customFormat) || p.OriginalValue == null) + return p.StringFormatted; + + Type t = p.Type; + string val = p.StringFormatted; + + // avoid repeated ToLower calls + string fmt = customFormat.Trim(); + string fmtLower = fmt.ToLowerInvariant(); + + // handle numeric and datetime formats first + try + { + if (t == typeof(int) || t == typeof(int?)) + return int.Parse(val, CultureInfo.InvariantCulture).ToString(fmt, CultureInfo.InvariantCulture); + if (t == typeof(double) || t == typeof(double?)) + return double.Parse(val, CultureInfo.InvariantCulture).ToString(fmt, CultureInfo.InvariantCulture); + if (t == typeof(long) || t == typeof(long?)) + return long.Parse(val, CultureInfo.InvariantCulture).ToString(fmt, CultureInfo.InvariantCulture); + if (t == typeof(float) || t == typeof(float?)) + return float.Parse(val, CultureInfo.InvariantCulture).ToString(fmt, CultureInfo.InvariantCulture); + if (t == typeof(decimal) || t == typeof(decimal?)) + return decimal.Parse(val, CultureInfo.InvariantCulture).ToString(fmt, CultureInfo.InvariantCulture); + if (t == typeof(DateTime) || t == typeof(DateTime?)) + return DateTime.Parse(val, CultureInfo.InvariantCulture).ToString(fmt, CultureInfo.InvariantCulture); + } + catch + { + // fall through if invalid format + } + + // custom string-based formats (single switch to avoid multiple ToLower() checks) + switch (fmtLower) + { + case "uppercase": return val?.ToUpperInvariant(); + case "lowercase": return val?.ToLowerInvariant(); + case "tomd5": return val?.ToMD5String(); + case "tobase64": return val?.ToBase64String(); + case "frombase64": return val?.FromBase64String(); + case "length": return val?.Length.ToString(CultureInfo.InvariantCulture); + case "titlecase": return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(val?.ToLowerInvariant() ?? string.Empty); + default: return val; + } + } + + private static T GetConvertibleValue(string value) where T : IConvertible + { + if (string.IsNullOrWhiteSpace(value) || string.Equals(value.Trim(), "null", StringComparison.OrdinalIgnoreCase)) + return default; + + return (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture); + } + + public static bool IsPropertyValueConditionPassed(this ExtractedObjProperty property, string valueComparer, string criteria) + { + if (property == null) + return false; + + try + { + Type t = property.Type; + object original = property.OriginalValue; + string crit = criteria?.Trim() ?? string.Empty; + + if (t == typeof(string)) + { + string v1 = (original?.ToString() ?? string.Empty).Trim().ToLowerInvariant(); + string v2 = (GetConvertibleValue(valueComparer) ?? string.Empty).Trim().ToLowerInvariant(); + switch (crit) + { + case "==": + return v1 == v2; + case "!=": + return v1 != v2; + default: + return string.Equals(v1, v2, StringComparison.OrdinalIgnoreCase); + } + } + + if (t == typeof(int) || t == typeof(double) || t == typeof(long) || + t == typeof(float) || t == typeof(decimal)) + { + double v1 = Convert.ToDouble(original ?? 0, CultureInfo.InvariantCulture); + double v2 = Convert.ToDouble(GetConvertibleValue(valueComparer), CultureInfo.InvariantCulture); + + switch (crit) + { + case "==": return v1 == v2; + case "!=": return v1 != v2; + case ">": return v1 > v2; + case ">=": return v1 >= v2; + case "<": return v1 < v2; + case "<=": return v1 <= v2; + default: return false; + } + } + + if (t == typeof(DateTime)) + { + DateTime v1 = Convert.ToDateTime(original, CultureInfo.InvariantCulture); + DateTime v2 = Convert.ToDateTime(GetConvertibleValue(valueComparer), CultureInfo.InvariantCulture); + + switch (crit) + { + case "==": return v1 == v2; + case "!=": return v1 != v2; + case ">": return v1 > v2; + case ">=": return v1 >= v2; + case "<": return v1 < v2; + case "<=": return v1 <= v2; + default: return false; + } + } + + if (t == typeof(bool)) + { + bool v1 = Convert.ToBoolean(original, CultureInfo.InvariantCulture); + bool v2 = Convert.ToBoolean(GetConvertibleValue(valueComparer)); + return crit == "==" ? v1 == v2 : crit == "!=" && v1 != v2; + } + + if (property.IsEnumerableObject) + { + int v1 = original is IEnumerable enumerable ? enumerable.Count() : 0; + double v2 = Convert.ToDouble(GetConvertibleValue(valueComparer), CultureInfo.InvariantCulture); + + switch (crit) + { + case "==": return v1 == v2; + case "!=": return v1 != v2; + case ">": return v1 > v2; + case ">=": return v1 >= v2; + case "<": return v1 < v2; + case "<=": return v1 <= v2; + default: return false; + } + } + + return false; + } + catch + { + return false; + } + } + } +} diff --git a/ObjectSemantics.NET/Engine/Extensions/ReplaceCodeExtensions.cs b/ObjectSemantics.NET/Engine/Extensions/ReplaceCodeExtensions.cs new file mode 100644 index 0000000..7e74eff --- /dev/null +++ b/ObjectSemantics.NET/Engine/Extensions/ReplaceCodeExtensions.cs @@ -0,0 +1,36 @@ +using ObjectSemantics.NET.Engine.Models; + +namespace ObjectSemantics.NET.Engine.Extensions +{ + public static class ReplaceCodeExtensions + { + public static string GetTargetPropertyName(this ReplaceCode code) + { + if (code == null) return string.Empty; + + if (string.IsNullOrEmpty(code.ReplaceCommand)) + return string.Empty; + + int colonIndex = code.ReplaceCommand.IndexOf(':'); + return colonIndex > 0 ? code.ReplaceCommand.Substring(0, colonIndex).Trim() : code.ReplaceCommand.Trim(); + } + + public static string GetFormattingCommand(this ReplaceCode code) + { + if (code == null) return string.Empty; + + if (string.IsNullOrEmpty(code.ReplaceCommand)) + return string.Empty; + + // Find the colon separating the target and the formatting command + int colonIndex = code.ReplaceCommand.IndexOf(':'); + if (colonIndex < 0 || colonIndex >= code.ReplaceCommand.Length - 1) + return string.Empty; + + // Extract everything after the first colon + string afterColon = code.ReplaceCommand.Substring(colonIndex + 1).Trim(); + + return afterColon; + } + } +} diff --git a/ObjectSemantics.NET/Extensions/StringExtensions.cs b/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs similarity index 98% rename from ObjectSemantics.NET/Extensions/StringExtensions.cs rename to ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs index b028e36..7e898c0 100644 --- a/ObjectSemantics.NET/Extensions/StringExtensions.cs +++ b/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs @@ -1,7 +1,7 @@ using System; using System.Text; -namespace ObjectSemantics.NET +namespace ObjectSemantics.NET.Engine.Extensions { public static class StringExtensions { @@ -17,6 +17,7 @@ public static string RemoveLastInstanceOfString(this string value, string remove 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) diff --git a/ObjectSemantics.NET/Engine/Models/EngineRunnerTemplate.cs b/ObjectSemantics.NET/Engine/Models/EngineRunnerTemplate.cs new file mode 100644 index 0000000..43d875a --- /dev/null +++ b/ObjectSemantics.NET/Engine/Models/EngineRunnerTemplate.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace ObjectSemantics.NET.Engine.Models +{ + public class EngineRunnerTemplate + { + public string Template { get; set; } + public List ReplaceObjLoopCodes { get; set; } = new List(); + public List ReplaceCodes { get; set; } = new List(); + public List ReplaceIfConditionCodes { get; set; } = new List(); + } +} diff --git a/ObjectSemantics.NET/Engine/Models/ExtractedObjProperty.cs b/ObjectSemantics.NET/Engine/Models/ExtractedObjProperty.cs new file mode 100644 index 0000000..ff116c7 --- /dev/null +++ b/ObjectSemantics.NET/Engine/Models/ExtractedObjProperty.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections; + +namespace ObjectSemantics.NET.Engine.Models +{ + public class ExtractedObjProperty + { + public Type Type { get; set; } + public string Name { get; set; } + public object OriginalValue { get; set; } + public string StringFormatted { get { return string.Format("{0}", OriginalValue); } } + public bool IsEnumerableObject + { + get { return typeof(IEnumerable).IsAssignableFrom(Type) && Type != typeof(string); } + } + public bool IsClassObject + { + get { return Type.IsClass && Type != typeof(string); } + } + } +} \ No newline at end of file diff --git a/ObjectSemantics.NET/Engine/Models/ReplaceCode.cs b/ObjectSemantics.NET/Engine/Models/ReplaceCode.cs new file mode 100644 index 0000000..c9c212e --- /dev/null +++ b/ObjectSemantics.NET/Engine/Models/ReplaceCode.cs @@ -0,0 +1,18 @@ +namespace ObjectSemantics.NET.Engine.Models +{ + public class ReplaceCode + { + public string ReplaceRef { get; set; } + public string ReplaceCommand { get; set; } + public string TargetPropertyName + { + get + { + if (string.IsNullOrEmpty(ReplaceCommand)) + return string.Empty; + int colonIndex = ReplaceCommand.IndexOf(':'); + return colonIndex > 0 ? ReplaceCommand.Substring(0, colonIndex).Trim() : ReplaceCommand.Trim(); + } + } + } +} \ No newline at end of file diff --git a/ObjectSemantics.NET/Engine/Models/ReplaceIfOperationCode.cs b/ObjectSemantics.NET/Engine/Models/ReplaceIfOperationCode.cs new file mode 100644 index 0000000..7f31ea9 --- /dev/null +++ b/ObjectSemantics.NET/Engine/Models/ReplaceIfOperationCode.cs @@ -0,0 +1,12 @@ +namespace ObjectSemantics.NET.Engine.Models +{ + public class ReplaceIfOperationCode + { + public string IfPropertyName { get; set; } + public string IfOperationType { get; set; } + public string IfOperationValue { get; set; } + public string ReplaceRef { get; set; } + public string IfOperationTrueTemplate { get; set; } = string.Empty; + public string IfOperationFalseTemplate { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/ObjectSemantics.NET/Engine/Models/ReplaceObjLoopCode.cs b/ObjectSemantics.NET/Engine/Models/ReplaceObjLoopCode.cs new file mode 100644 index 0000000..dae81f1 --- /dev/null +++ b/ObjectSemantics.NET/Engine/Models/ReplaceObjLoopCode.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace ObjectSemantics.NET.Engine.Models +{ + public class ReplaceObjLoopCode + { + public string ReplaceRef { get; set; } + public string TargetObjectName { get; set; } + public string ObjLoopTemplate { get; set; } + public List ReplaceObjCodes { get; set; } = new List(); + } +} diff --git a/ObjectSemantics.NET/Extensions/ExtractedObjPropertyExtensions.cs b/ObjectSemantics.NET/Extensions/ExtractedObjPropertyExtensions.cs deleted file mode 100644 index 4ffab68..0000000 --- a/ObjectSemantics.NET/Extensions/ExtractedObjPropertyExtensions.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Security; - -namespace ObjectSemantics.NET -{ - public static class ExtractedObjPropertyExtensions - { - public static string GetPropertyDisplayString(this ExtractedObjProperty p, string stringFormatting, TemplateMapperOptions templateMapperOptions) - { - string formattedPropertyString = GetAppliedPropertyFormatting(p, stringFormatting); - //Apply Options to Property value string - if (templateMapperOptions == null) return formattedPropertyString; - if (templateMapperOptions.XmlCharEscaping) - formattedPropertyString = SecurityElement.Escape(formattedPropertyString); - return formattedPropertyString; - } - private static string GetAppliedPropertyFormatting(this ExtractedObjProperty p, string customFormattingValue) - { - if (string.IsNullOrWhiteSpace(customFormattingValue) || p.OriginalValue == null) - return p.StringFormatted; - if (p.Type.Equals(typeof(int)) || p.Type.Equals(typeof(int?))) - return int.Parse(p.StringFormatted).ToString(customFormattingValue); - else if (p.Type.Equals(typeof(double)) || p.Type.Equals(typeof(double?))) - return double.Parse(p.StringFormatted).ToString(customFormattingValue); - else if (p.Type.Equals(typeof(long)) || p.Type.Equals(typeof(long?))) - return long.Parse(p.StringFormatted).ToString(customFormattingValue); - else if (p.Type.Equals(typeof(float)) || p.Type.Equals(typeof(float?))) - return float.Parse(p.StringFormatted).ToString(customFormattingValue); - else if (p.Type.Equals(typeof(decimal)) || p.Type.Equals(typeof(decimal?))) - return decimal.Parse(p.StringFormatted).ToString(customFormattingValue); - else if (p.Type.Equals(typeof(DateTime)) || p.Type.Equals(typeof(DateTime?))) - return DateTime.Parse(p.StringFormatted).ToString(customFormattingValue); - //Custom Formats - else if (customFormattingValue.ToLower().Equals("uppercase")) - return p.StringFormatted?.ToUpper(); - else if (customFormattingValue.ToLower().Equals("lowercase")) - return p.StringFormatted?.ToLower(); - else if (customFormattingValue.ToLower().Equals("tomd5")) - return p.StringFormatted.ToMD5String(); - else if (customFormattingValue.ToLower().Equals("tobase64")) - return p.StringFormatted.ToBase64String(); - else if (customFormattingValue.ToLower().Equals("frombase64")) - return p.StringFormatted.FromBase64String(); - else if (customFormattingValue.ToLower().Equals("length")) - return p.StringFormatted?.Length.ToString(); - else if (customFormattingValue.ToLower().Equals("titlecase")) - return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(p.StringFormatted?.ToLower() ?? string.Empty); - else - return p.StringFormatted; - } - - private static T GetConvertibleValue(string value) where T : IConvertible - { - return (string.IsNullOrEmpty(value) || value?.ToLower()?.Trim() == "null") ? default : (T)Convert.ChangeType(value, typeof(T)); - } - public static bool IsPropertyValueConditionPassed(this ExtractedObjProperty property, string valueComparer, string criteria) - { - try - { - if (property == null) return false; - else if (property.Type == typeof(string)) - { - string v1 = property.OriginalValue?.ToString()?.Trim().ToLower() ?? string.Empty; - string v2 = GetConvertibleValue(valueComparer)?.Trim().ToLower() ?? string.Empty; - switch (criteria) - { - case "==": return v1 == v2; - case "!=": return v1 != v2; - default: - return string.Compare(v1, v2, true) == 0; - } - } - else if (property.Type == typeof(int) || property.Type == typeof(double) || property.Type == typeof(long) || property.Type == typeof(float) || property.Type == typeof(decimal)) - { - double v1 = Convert.ToDouble(property.OriginalValue ?? "0"); - double v2 = Convert.ToDouble(GetConvertibleValue(valueComparer)); - switch (criteria) - { - case "==": return v1 == v2; - case "!=": return v1 != v2; - case ">": return v1 > v2; - case ">=": return v1 >= v2; - case "<": return v1 < v2; - case "<=": return v1 <= v2; - default: return false; - } - } - else if (property.Type == typeof(DateTime)) - { - DateTime v1 = Convert.ToDateTime(property.OriginalValue); - DateTime v2 = Convert.ToDateTime(GetConvertibleValue(valueComparer)); - switch (criteria) - { - case "==": return v1 == v2; - case "!=": return v1 != v2; - case ">": return v1 > v2; - case ">=": return v1 >= v2; - case "<": return v1 < v2; - case "<=": return v1 <= v2; - default: return false; - } - } - else if (property.Type == typeof(bool)) - { - bool v1 = Convert.ToBoolean(property.OriginalValue); - bool v2 = Convert.ToBoolean(GetConvertibleValue(valueComparer)); - switch (criteria) - { - case "==": return v1 == v2; - case "!=": return v1 != v2; - default: return false; - } - } - else if (property.IsEnumerableObject) - { - int v1 = (property.OriginalValue == null) ? 0 : ((IEnumerable)property.OriginalValue).Count(); - double v2 = Convert.ToDouble(GetConvertibleValue(valueComparer)); - switch (criteria) - { - case "==": return v1 == v2; - case "!=": return v1 != v2; - case ">": return v1 > v2; - case ">=": return v1 >= v2; - case "<": return v1 < v2; - case "<=": return v1 <= v2; - default: return false; - } - } - else - return false; - } - catch { return false; } - } - } -} diff --git a/ObjectSemantics.NET/ObjectSemantics.NET.csproj b/ObjectSemantics.NET/ObjectSemantics.NET.csproj index 89c6572..2f1edc7 100644 --- a/ObjectSemantics.NET/ObjectSemantics.NET.csproj +++ b/ObjectSemantics.NET/ObjectSemantics.NET.csproj @@ -3,7 +3,7 @@ netstandard2.0 A library that allows you to Maps properties from a source object or class to a template string and returns the result. This is useful for dynamically generating strings based on object properties. - Crudsoft Technologies @ 2023 + Crudsoft Technologies @ 2025 https://github.com/swagfin/ObjectSemantics.NET icon.jpg @@ -28,12 +28,6 @@ False - - - - - - True diff --git a/ObjectSemantics.NET/ObjectSemanticsKeyValue.cs b/ObjectSemantics.NET/ObjectSemanticsKeyValue.cs index feb8ea3..9507d1d 100644 --- a/ObjectSemantics.NET/ObjectSemanticsKeyValue.cs +++ b/ObjectSemantics.NET/ObjectSemanticsKeyValue.cs @@ -1,11 +1,15 @@ -using System; - -namespace ObjectSemantics.NET +namespace ObjectSemantics.NET { public class ObjectSemanticsKeyValue { + public ObjectSemanticsKeyValue() { } + public ObjectSemanticsKeyValue(string key, object value) + { + Key = key; + Value = value; + } + public string Key { get; set; } public object Value { get; set; } - public Type Type { get { return Value.GetType(); } } } } diff --git a/ObjectSemantics.NET/TemplateMapper.cs b/ObjectSemantics.NET/TemplateMapper.cs index 1a6a8e9..6ad839b 100644 --- a/ObjectSemantics.NET/TemplateMapper.cs +++ b/ObjectSemantics.NET/TemplateMapper.cs @@ -1,11 +1,12 @@ -using System; +using ObjectSemantics.NET.Engine; +using ObjectSemantics.NET.Engine.Models; +using System; using System.Collections.Generic; namespace ObjectSemantics.NET { public static class TemplateMapper { - /// /// Generate a Data Template From Object Properties /// @@ -33,9 +34,8 @@ public static class TemplateMapper if (record == null) return string.Empty; if (template == null) throw new Exception("Template Object can't be NULL"); if (options == null) options = new TemplateMapperOptions(); - TemplatedContent templatedContent = GavinsAlgorithim.GenerateTemplateFromFileContents(template.FileContents, options); - if (templatedContent == null) throw new Exception($"Error Generating template from specified Template Name: {template.Name}"); - return GavinsAlgorithim.GenerateFromTemplate(record, templatedContent, additionalKeyValues, options); + EngineRunnerTemplate runnerTemplate = EngineAlgorithim.GenerateRunnerTemplate(template.FileContents); + return runnerTemplate == null ? throw new Exception($"Error Generating template from specified Template Name: {template.Name}") : EngineAlgorithim.GenerateFromTemplate(record, runnerTemplate, additionalKeyValues, options); } } } \ No newline at end of file From 70da02076f610842dd3fa6aad7748a9afb4975cc Mon Sep 17 00:00:00 2001 From: "George Njeri (Swagfin)" Date: Mon, 6 Oct 2025 21:10:22 +0300 Subject: [PATCH 02/12] chore: minor refactoring --- ObjectSemantics.NET/Engine/EngineAlgorithim.cs | 2 +- .../Engine/Extensions/ExtractedObjPropertyExtensions.cs | 2 +- ObjectSemantics.NET/Engine/Extensions/ReplaceCodeExtensions.cs | 2 +- ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs | 2 +- ObjectSemantics.NET/Engine/Models/EngineRunnerTemplate.cs | 2 +- ObjectSemantics.NET/Engine/Models/ExtractedObjProperty.cs | 2 +- ObjectSemantics.NET/Engine/Models/ReplaceCode.cs | 2 +- ObjectSemantics.NET/Engine/Models/ReplaceIfOperationCode.cs | 2 +- ObjectSemantics.NET/Engine/Models/ReplaceObjLoopCode.cs | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ObjectSemantics.NET/Engine/EngineAlgorithim.cs b/ObjectSemantics.NET/Engine/EngineAlgorithim.cs index 45840c6..d4fb9ed 100644 --- a/ObjectSemantics.NET/Engine/EngineAlgorithim.cs +++ b/ObjectSemantics.NET/Engine/EngineAlgorithim.cs @@ -11,7 +11,7 @@ namespace ObjectSemantics.NET.Engine { - public static class EngineAlgorithim + internal static class EngineAlgorithim { private static readonly ConcurrentDictionary PropertyCache = new ConcurrentDictionary(); diff --git a/ObjectSemantics.NET/Engine/Extensions/ExtractedObjPropertyExtensions.cs b/ObjectSemantics.NET/Engine/Extensions/ExtractedObjPropertyExtensions.cs index c0b041e..e596c41 100644 --- a/ObjectSemantics.NET/Engine/Extensions/ExtractedObjPropertyExtensions.cs +++ b/ObjectSemantics.NET/Engine/Extensions/ExtractedObjPropertyExtensions.cs @@ -7,7 +7,7 @@ namespace ObjectSemantics.NET.Engine.Extensions { - public static class ExtractedObjPropertyExtensions + internal static class ExtractedObjPropertyExtensions { public static string GetPropertyDisplayString(this ExtractedObjProperty p, string stringFormatting, TemplateMapperOptions options) { diff --git a/ObjectSemantics.NET/Engine/Extensions/ReplaceCodeExtensions.cs b/ObjectSemantics.NET/Engine/Extensions/ReplaceCodeExtensions.cs index 7e74eff..9595e26 100644 --- a/ObjectSemantics.NET/Engine/Extensions/ReplaceCodeExtensions.cs +++ b/ObjectSemantics.NET/Engine/Extensions/ReplaceCodeExtensions.cs @@ -2,7 +2,7 @@ namespace ObjectSemantics.NET.Engine.Extensions { - public static class ReplaceCodeExtensions + internal static class ReplaceCodeExtensions { public static string GetTargetPropertyName(this ReplaceCode code) { diff --git a/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs b/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs index 7e898c0..a0d49eb 100644 --- a/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs +++ b/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs @@ -3,7 +3,7 @@ namespace ObjectSemantics.NET.Engine.Extensions { - public static class StringExtensions + internal static class StringExtensions { public static string ReplaceFirstOccurrence(this string text, string search, string replace) { diff --git a/ObjectSemantics.NET/Engine/Models/EngineRunnerTemplate.cs b/ObjectSemantics.NET/Engine/Models/EngineRunnerTemplate.cs index 43d875a..6f3a1a2 100644 --- a/ObjectSemantics.NET/Engine/Models/EngineRunnerTemplate.cs +++ b/ObjectSemantics.NET/Engine/Models/EngineRunnerTemplate.cs @@ -2,7 +2,7 @@ namespace ObjectSemantics.NET.Engine.Models { - public class EngineRunnerTemplate + internal class EngineRunnerTemplate { public string Template { get; set; } public List ReplaceObjLoopCodes { get; set; } = new List(); diff --git a/ObjectSemantics.NET/Engine/Models/ExtractedObjProperty.cs b/ObjectSemantics.NET/Engine/Models/ExtractedObjProperty.cs index ff116c7..126a677 100644 --- a/ObjectSemantics.NET/Engine/Models/ExtractedObjProperty.cs +++ b/ObjectSemantics.NET/Engine/Models/ExtractedObjProperty.cs @@ -3,7 +3,7 @@ namespace ObjectSemantics.NET.Engine.Models { - public class ExtractedObjProperty + internal class ExtractedObjProperty { public Type Type { get; set; } public string Name { get; set; } diff --git a/ObjectSemantics.NET/Engine/Models/ReplaceCode.cs b/ObjectSemantics.NET/Engine/Models/ReplaceCode.cs index c9c212e..947ba1b 100644 --- a/ObjectSemantics.NET/Engine/Models/ReplaceCode.cs +++ b/ObjectSemantics.NET/Engine/Models/ReplaceCode.cs @@ -1,6 +1,6 @@ namespace ObjectSemantics.NET.Engine.Models { - public class ReplaceCode + internal class ReplaceCode { public string ReplaceRef { get; set; } public string ReplaceCommand { get; set; } diff --git a/ObjectSemantics.NET/Engine/Models/ReplaceIfOperationCode.cs b/ObjectSemantics.NET/Engine/Models/ReplaceIfOperationCode.cs index 7f31ea9..59a8949 100644 --- a/ObjectSemantics.NET/Engine/Models/ReplaceIfOperationCode.cs +++ b/ObjectSemantics.NET/Engine/Models/ReplaceIfOperationCode.cs @@ -1,6 +1,6 @@ namespace ObjectSemantics.NET.Engine.Models { - public class ReplaceIfOperationCode + internal class ReplaceIfOperationCode { public string IfPropertyName { get; set; } public string IfOperationType { get; set; } diff --git a/ObjectSemantics.NET/Engine/Models/ReplaceObjLoopCode.cs b/ObjectSemantics.NET/Engine/Models/ReplaceObjLoopCode.cs index dae81f1..15b15d4 100644 --- a/ObjectSemantics.NET/Engine/Models/ReplaceObjLoopCode.cs +++ b/ObjectSemantics.NET/Engine/Models/ReplaceObjLoopCode.cs @@ -2,7 +2,7 @@ namespace ObjectSemantics.NET.Engine.Models { - public class ReplaceObjLoopCode + internal class ReplaceObjLoopCode { public string ReplaceRef { get; set; } public string TargetObjectName { get; set; } From 8ec9c1d25f2e8516932e44601faac954a662e02f Mon Sep 17 00:00:00 2001 From: "George Njeri (Swagfin)" Date: Mon, 6 Oct 2025 23:20:44 +0300 Subject: [PATCH 03/12] chore: improved unit tests --- .../BasicMappingTests.cs | 161 +++---- .../CharacterEscapingTests.cs | 98 ++-- .../EnumerableLoopTests.cs | 449 +++++++++--------- ObjectSemantics.NET.Tests/IfConditionTests.cs | 234 ++++----- .../MoqFiles/template-example.txt | 1 - ObjectSemantics.NET.Tests/MoqModels/Car.cs | 23 + .../ObjectSemantics.NET.Tests.csproj | 58 +-- .../PropertyBooleanMapTests.cs | 35 ++ .../PropertyNullableTests.cs | 128 ++--- .../PropertyNumberMapTests.cs | 74 +++ .../PropertyStringMapTests.cs | 84 ++++ .../StringFormattingTests.cs | 292 +++++------- .../Engine/EngineAlgorithim.cs | 12 +- .../ObjectSemantics.NET.csproj | 6 +- .../ObjectSemanticsKeyValue.cs | 15 - .../ObjectSemanticsTemplate.cs | 12 - ObjectSemantics.NET/TemplateMapper.cs | 28 +- 17 files changed, 922 insertions(+), 788 deletions(-) delete mode 100644 ObjectSemantics.NET.Tests/MoqFiles/template-example.txt create mode 100644 ObjectSemantics.NET.Tests/MoqModels/Car.cs create mode 100644 ObjectSemantics.NET.Tests/PropertyBooleanMapTests.cs create mode 100644 ObjectSemantics.NET.Tests/PropertyNumberMapTests.cs create mode 100644 ObjectSemantics.NET.Tests/PropertyStringMapTests.cs delete mode 100644 ObjectSemantics.NET/ObjectSemanticsKeyValue.cs delete mode 100644 ObjectSemantics.NET/ObjectSemanticsTemplate.cs diff --git a/ObjectSemantics.NET.Tests/BasicMappingTests.cs b/ObjectSemantics.NET.Tests/BasicMappingTests.cs index 083a492..efc6403 100644 --- a/ObjectSemantics.NET.Tests/BasicMappingTests.cs +++ b/ObjectSemantics.NET.Tests/BasicMappingTests.cs @@ -1,92 +1,85 @@ -using ObjectSemantics.NET.Tests.MoqModels; -using System.Collections.Generic; -using Xunit; +//using ObjectSemantics.NET.Tests.MoqModels; +//using System.Collections.Generic; +//using Xunit; -namespace ObjectSemantics.NET.Tests -{ - public class BasicMappingTests - { - [Fact] - public void Should_Map_Object_To_Template_From_TemplateObject() - { - //Create Model - Student student = new Student - { - StudentName = "George Waynne", - Balance = 2510 - }; - var template = new ObjectSemanticsTemplate - { - FileContents = @"My Name is: {{ StudentName }}" - }; - string generatedTemplate = template.Map(student); - string expectedString = "My Name is: George Waynne"; - Assert.Equal(expectedString, generatedTemplate, false, true, true); - } +//namespace ObjectSemantics.NET.Tests +//{ +// public class BasicMappingTests +// { +// [Fact] +// public void Should_Map_Object_To_Template_From_TemplateObject() +// { +// //Create Model +// Student student = new Student +// { +// StudentName = "George Waynne", +// Balance = 2510 +// }; +// string generatedTemplate = student.Map(@"My Name is: {{ StudentName }}"); +// string expectedString = "My Name is: George Waynne"; +// Assert.Equal(expectedString, generatedTemplate, false, true, true); +// } - [Fact] - public void Should_Map_Additional_Parameters() - { - //Create Model - Student student = new Student - { - StudentName = "George Waynne" - }; - //Template - var template = new ObjectSemanticsTemplate - { - FileContents = @"My Name is: {{ StudentName }} | CompanyName: {{ CompanyName }} | CompanyEmail: {{ CompanyEmail }} | Employees: {{ Employees }}" - }; - //Additional Parameters - List additionalParams = new List - { - new ObjectSemanticsKeyValue{ Key ="CompanyName", Value= "TEST INC." }, - new ObjectSemanticsKeyValue{ Key ="CompanyEmail", Value= "test.inc@test.com" }, - new ObjectSemanticsKeyValue{ Key ="Employees", Value= 1289 }, - }; - string generatedTemplate = template.Map(student, additionalParams); - string expectedString = "My Name is: George Waynne | CompanyName: TEST INC. | CompanyEmail: test.inc@test.com | Employees: 1289"; - Assert.Equal(expectedString, generatedTemplate, false, true, true); - } +// [Fact] +// public void Should_Map_Additional_Parameters() +// { +// //Create Model +// Student student = new Student +// { +// StudentName = "George Waynne" +// }; - [Fact] - public void Should_Return_Unknown_Properties_If_Not_Found_In_Object() - { - //Create Model - Student student = new Student - { - StudentName = "George Waynne" - }; - //Template - var template = new ObjectSemanticsTemplate - { - FileContents = @"Unknown Object example: {{StudentIdentityCardXyx}}" - }; - string generatedTemplate = template.Map(student); - string expectedString = "Unknown Object example: {{ StudentIdentityCardXyx }}"; - Assert.Equal(expectedString, generatedTemplate, false, true, true); - } +// //Additional Parameters +// Dictionary additionalParams = new Dictionary +// { +// { "CompanyName","TEST INC."}, +// { "CompanyEmail","test.inc@test.com"}, +// { "Employees", 1289 } +// }; +// string generatedTemplate = student.Map(@"My Name is: {{ StudentName }} | CompanyName: {{ CompanyName }} | CompanyEmail: {{ CompanyEmail }} | Employees: {{ Employees }}", additionalParams); +// string expectedString = "My Name is: George Waynne | CompanyName: TEST INC. | CompanyEmail: test.inc@test.com | Employees: 1289"; +// Assert.Equal(expectedString, generatedTemplate, false, true, true); +// } - [Fact] - public void Should_Ignore_Whitespaces_Inside_CurlyBrackets() - { - //Create Model - Student student = new Student - { - StudentName = "George Waynne" - }; - //Template - var template = new ObjectSemanticsTemplate - { - FileContents = @"StudentName is: {{ StudentName }}" - }; - string generatedTemplate = template.Map(student); - string expectedString = "StudentName is: George Waynne"; - Assert.Equal(expectedString, generatedTemplate, false, true, true); - } +// [Fact] +// public void Should_Return_Unknown_Properties_If_Not_Found_In_Object() +// { +// //Create Model +// Student student = new Student +// { +// StudentName = "George Waynne" +// }; +// //Template +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"Unknown Object example: {{StudentIdentityCardXyx}}" +// }; +// string generatedTemplate = template.Map(student); +// string expectedString = "Unknown Object example: {{ StudentIdentityCardXyx }}"; +// Assert.Equal(expectedString, generatedTemplate, false, true, true); +// } - } -} + +// [Fact] +// public void Should_Ignore_Whitespaces_Inside_CurlyBrackets() +// { +// //Create Model +// Student student = new Student +// { +// StudentName = "George Waynne" +// }; +// //Template +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"StudentName is: {{ StudentName }}" +// }; +// string generatedTemplate = template.Map(student); +// string expectedString = "StudentName is: George Waynne"; +// Assert.Equal(expectedString, generatedTemplate, false, true, true); +// } + +// } +//} diff --git a/ObjectSemantics.NET.Tests/CharacterEscapingTests.cs b/ObjectSemantics.NET.Tests/CharacterEscapingTests.cs index e24103e..9008d5c 100644 --- a/ObjectSemantics.NET.Tests/CharacterEscapingTests.cs +++ b/ObjectSemantics.NET.Tests/CharacterEscapingTests.cs @@ -1,52 +1,52 @@ -using ObjectSemantics.NET.Tests.MoqModels; -using System.Collections.Generic; -using Xunit; +//using ObjectSemantics.NET.Tests.MoqModels; +//using System.Collections.Generic; +//using Xunit; -namespace ObjectSemantics.NET.Tests -{ - public class CharacterEscapingTests - { +//namespace ObjectSemantics.NET.Tests +//{ +// public class CharacterEscapingTests +// { - [Fact] - public void Should_Escape_Xml_Char_If_Option_Is_Enabled() - { - //Create Model - Student student = new Student { StudentName = "I've got \"special\" < & also >" }; - var template = new ObjectSemanticsTemplate - { - FileContents = @"{{ StudentName }}" - }; - string generatedTemplate = template.Map(student, null, new TemplateMapperOptions - { - XmlCharEscaping = true - }); - string expectedString = "I've got "special" < & also >"; - Assert.Equal(expectedString, generatedTemplate, false, true, true); - } +// [Fact] +// public void Should_Escape_Xml_Char_If_Option_Is_Enabled() +// { +// //Create Model +// Student student = new Student { StudentName = "I've got \"special\" < & also >" }; +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"{{ StudentName }}" +// }; +// string generatedTemplate = template.Map(student, null, new TemplateMapperOptions +// { +// XmlCharEscaping = true +// }); +// string expectedString = "I've got "special" < & also >"; +// Assert.Equal(expectedString, generatedTemplate, false, true, true); +// } - [Fact] - public void Should_Escape_Xml_Char_In_LOOPS_If_Option_Is_Enabled() - { - //Create Model - Student student = new Student - { - Invoices = new List - { - new Invoice{ Narration="I've got \"special\""}, - new Invoice{ Narration="I've got < & also >"} - } - }; - //Template - var template = new ObjectSemanticsTemplate - { - FileContents = @"{{ #foreach(invoices) }} [{{ Narration }}] {{ #endforeach }}" - }; - string generatedTemplate = template.Map(student, null, new TemplateMapperOptions - { - XmlCharEscaping = true - }); - string expectedResult = @" [I've got "special"] [I've got < & also >] "; - Assert.Equal(expectedResult, generatedTemplate, false, true, true); - } - } -} +// [Fact] +// public void Should_Escape_Xml_Char_In_LOOPS_If_Option_Is_Enabled() +// { +// //Create Model +// Student student = new Student +// { +// Invoices = new List +// { +// new Invoice{ Narration="I've got \"special\""}, +// new Invoice{ Narration="I've got < & also >"} +// } +// }; +// //Template +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"{{ #foreach(invoices) }} [{{ Narration }}] {{ #endforeach }}" +// }; +// string generatedTemplate = template.Map(student, null, new TemplateMapperOptions +// { +// XmlCharEscaping = true +// }); +// string expectedResult = @" [I've got "special"] [I've got < & also >] "; +// Assert.Equal(expectedResult, generatedTemplate, false, true, true); +// } +// } +//} diff --git a/ObjectSemantics.NET.Tests/EnumerableLoopTests.cs b/ObjectSemantics.NET.Tests/EnumerableLoopTests.cs index 415f7b9..209e83b 100644 --- a/ObjectSemantics.NET.Tests/EnumerableLoopTests.cs +++ b/ObjectSemantics.NET.Tests/EnumerableLoopTests.cs @@ -1,222 +1,227 @@ -using ObjectSemantics.NET.Tests.MoqModels; -using System; -using System.Collections.Generic; -using Xunit; - -namespace ObjectSemantics.NET.Tests -{ - public class EnumerableLoopTests - { - - [Fact] - public void Should_Map_Enumerable_Collection_In_Object() - { - //Create Model - Student student = new Student - { - StudentName = "John Doe", - Invoices = new List - { - new Invoice{ Id=2, RefNo="INV_002",Narration="Grade II Fees Invoice", Amount=2000, InvoiceDate= new DateTime(2023, 04, 01) }, - new Invoice{ Id=1, RefNo="INV_001",Narration="Grade I Fees Invoice", Amount=320, InvoiceDate= new DateTime(2022, 08, 01) } - } - }; - //Template - var template = new ObjectSemanticsTemplate - { - FileContents = @"{{ StudentName }} Invoices -{{ #foreach(invoices) }} - - {{ Id }} - {{ RefNo }} - {{ Narration }} - {{ Amount:N0 }} - {{ InvoiceDate:yyyy-MM-dd }} - -{{ #endforeach }}" - }; - string generatedTemplate = template.Map(student); - string expectedResult = @"John Doe Invoices - - - 2 - INV_002 - Grade II Fees Invoice - 2,000 - 2023-04-01 - - - - 1 - INV_001 - Grade I Fees Invoice - 320 - 2022-08-01 -"; - Assert.Equal(expectedResult, generatedTemplate, false, true, true); - } - - - [Fact] - public void Should_Map_Enumerable_Collection_SingleLine_Test() - { - //Create Model - Student student = new Student - { - StudentName = "John Doe", - Invoices = new List - { - new Invoice{ Id=2, RefNo="INV_002",Narration="Grade II Fees Invoice", Amount=2000, InvoiceDate= new DateTime(2023, 04, 01) }, - new Invoice{ Id=1, RefNo="INV_001",Narration="Grade I Fees Invoice", Amount=320, InvoiceDate= new DateTime(2022, 08, 01) } - } - }; - //Template - var template = new ObjectSemanticsTemplate - { - FileContents = @"{{ #foreach(invoices) }} [{{RefNo}}] {{ #endforeach }}" - }; - string generatedTemplate = template.Map(student); - string expectedResult = " [INV_002] [INV_001] "; - Assert.Equal(expectedResult, generatedTemplate, false, true, true); - } - - - [Fact] - public void Should_Map_Multiple_Same_Property_Enumerable_Collection_On_Same_Template() - { - //Create Model - Student student = new Student - { - StudentName = "John Doe", - Invoices = new List - { - new Invoice{ Id=2, RefNo="INV_002",Narration="Grade II Fees Invoice", Amount=2000, InvoiceDate= new DateTime(2023, 04, 01) }, - new Invoice{ Id=1, RefNo="INV_001",Narration="Grade I Fees Invoice", Amount=320, InvoiceDate= new DateTime(2022, 08, 01) } - } - }; - //Template - var template = new ObjectSemanticsTemplate - { - FileContents = @" -{{ StudentName }} Invoices -LOOP #1 -{{ #foreach(invoices) }} -
{{ Id }} On Loop #1
-{{ #endforeach }} -LOOP #2 -{{ #foreach(invoices) }} -
{{ Id }} On Loop #2
-{{ #endforeach }}" - }; - string generatedTemplate = template.Map(student); - string expectedResult = @" -John Doe Invoices -LOOP #1 - -
2 On Loop #1
- -
1 On Loop #1
-LOOP #2 - -
2 On Loop #2
- -
1 On Loop #2
"; - Assert.Equal(expectedResult, generatedTemplate, false, true, true); - } - - - [Fact] - public void Should_Map_Multiple_Different_Property_Enumerable_Collection_On_Same_Template() - { - //Create Model - Student student = new Student - { - StudentName = "John Doe", - Invoices = new List - { - new Invoice{ Id=2, RefNo="INV_002",Narration="Grade II Fees Invoice", Amount=2000, InvoiceDate= new DateTime(2023, 04, 01) }, - new Invoice{ Id=1, RefNo="INV_001",Narration="Grade I Fees Invoice", Amount=320, InvoiceDate= new DateTime(2022, 08, 01) } - }, - StudentClockInDetails = new List - { - new StudentClockInDetail{ LastClockedInDate = new DateTime(2024, 04, 01), LastClockedInPoints = 10 }, - new StudentClockInDetail{ LastClockedInDate = new DateTime(2024, 04, 02), LastClockedInPoints = 30 } - } - }; - //Template - var template = new ObjectSemanticsTemplate - { - FileContents = @" -{{ StudentName }} Invoices -LOOP #1 -{{ #foreach(invoices) }} -
{{ Id }} On Loop #1
-{{ #endforeach }} -LOOP #2 -{{ #foreach(studentClockInDetails) }} -
Got {{ LastClockedInPoints }} for {{ LastClockedInDate:yyyy-MM-dd }}
-{{ #endforeach }}" - }; - string generatedTemplate = template.Map(student); - string expectedResult = @" -John Doe Invoices -LOOP #1 - -
2 On Loop #1
- -
1 On Loop #1
-LOOP #2 - -
Got 10 for 2024-04-01
- -
Got 30 for 2024-04-02
"; - - Assert.Equal(expectedResult, generatedTemplate, false, true, true); - } - - [Fact] - public void Should_Map_Array_Of_String_With_Formatting() - { - //Create Model - Student student = new Student - { - ArrayOfString = new string[] - { - "String 001", - "String 002" - } - }; - //Template - var template = new ObjectSemanticsTemplate - { - FileContents = @"{{ #foreach(ArrayOfString) }} {{ . }} | {{ .:uppercase }} {{ #endforeach }}" - }; - string generatedTemplate = template.Map(student); - string expectedResult = " String 001 | STRING 001 String 002 | STRING 002 "; - Assert.Equal(expectedResult, generatedTemplate, false, true, true); - } - - - [Fact] - public void Should_Map_Array_Of_Double_With_Formatting_Test() - { - //Create Model - Student student = new Student - { - ArrayOfDouble = new double[] - { - 1000.15, - 2000.22 - } - }; - //Template - var template = new ObjectSemanticsTemplate - { - FileContents = @"{{ #foreach(ArrayOfDouble) }} {{ . }} | {{ .:N0 }} {{ #endforeach }}" - }; - string generatedTemplate = template.Map(student); - string expectedResult = " 1000.15 | 1,000 2000.22 | 2,000 "; - Assert.Equal(expectedResult, generatedTemplate, false, true, true); - } - } -} +//using ObjectSemantics.NET.Tests.MoqModels; +//using System; +//using System.Collections.Generic; +//using Xunit; + +//namespace ObjectSemantics.NET.Tests +//{ +// public class EnumerableLoopTests +// { + +// [Fact] +// public void Should_Map_Enumerable_Collection_In_Object() +// { +// //Create Model +// Student student = new Student +// { +// StudentName = "John Doe", +// Invoices = new List +// { +// new Invoice{ Id=2, RefNo="INV_002",Narration="Grade II Fees Invoice", Amount=2000, InvoiceDate= new DateTime(2023, 04, 01) }, +// new Invoice{ Id=1, RefNo="INV_001",Narration="Grade I Fees Invoice", Amount=320, InvoiceDate= new DateTime(2022, 08, 01) } +// } +// }; +// //Template +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"{{ StudentName }} Invoices +//{{ #foreach(invoices) }} +// +// {{ Id }} +// {{ RefNo }} +// {{ Narration }} +// {{ Amount:N0 }} +// {{ InvoiceDate:yyyy-MM-dd }} +// +//{{ #endforeach }}" +// }; +// string generatedTemplate = template.Map(student); +// string expectedResult = @"John Doe Invoices + +// +// 2 +// INV_002 +// Grade II Fees Invoice +// 2,000 +// 2023-04-01 +// + +// +// 1 +// INV_001 +// Grade I Fees Invoice +// 320 +// 2022-08-01 +// +//"; +// Assert.Equal(expectedResult, generatedTemplate, false, true, true); +// } + + +// [Fact] +// public void Should_Map_Enumerable_Collection_SingleLine_Test() +// { +// //Create Model +// Student student = new Student +// { +// StudentName = "John Doe", +// Invoices = new List +// { +// new Invoice{ Id=2, RefNo="INV_002",Narration="Grade II Fees Invoice", Amount=2000, InvoiceDate= new DateTime(2023, 04, 01) }, +// new Invoice{ Id=1, RefNo="INV_001",Narration="Grade I Fees Invoice", Amount=320, InvoiceDate= new DateTime(2022, 08, 01) } +// } +// }; +// //Template +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"{{ #foreach(invoices) }} [{{RefNo}}] {{ #endforeach }}" +// }; +// string generatedTemplate = template.Map(student); +// string expectedResult = " [INV_002] [INV_001] "; +// Assert.Equal(expectedResult, generatedTemplate, false, true, true); +// } + + +// [Fact] +// public void Should_Map_Multiple_Same_Property_Enumerable_Collection_On_Same_Template() +// { +// //Create Model +// Student student = new Student +// { +// StudentName = "John Doe", +// Invoices = new List +// { +// new Invoice{ Id=2, RefNo="INV_002",Narration="Grade II Fees Invoice", Amount=2000, InvoiceDate= new DateTime(2023, 04, 01) }, +// new Invoice{ Id=1, RefNo="INV_001",Narration="Grade I Fees Invoice", Amount=320, InvoiceDate= new DateTime(2022, 08, 01) } +// } +// }; +// //Template +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @" +//{{ StudentName }} Invoices +//LOOP #1 +//{{ #foreach(invoices) }} +//
{{ Id }} On Loop #1
+//{{ #endforeach }} +//LOOP #2 +//{{ #foreach(invoices) }} +//
{{ Id }} On Loop #2
+//{{ #endforeach }}" +// }; +// string generatedTemplate = template.Map(student); +// string expectedResult = @" +//John Doe Invoices +//LOOP #1 + +//
2 On Loop #1
+ +//
1 On Loop #1
+ +//LOOP #2 + +//
2 On Loop #2
+ +//
1 On Loop #2
+//"; +// Assert.Equal(expectedResult, generatedTemplate, false, true, true); +// } + + +// [Fact] +// public void Should_Map_Multiple_Different_Property_Enumerable_Collection_On_Same_Template() +// { +// //Create Model +// Student student = new Student +// { +// StudentName = "John Doe", +// Invoices = new List +// { +// new Invoice{ Id=2, RefNo="INV_002",Narration="Grade II Fees Invoice", Amount=2000, InvoiceDate= new DateTime(2023, 04, 01) }, +// new Invoice{ Id=1, RefNo="INV_001",Narration="Grade I Fees Invoice", Amount=320, InvoiceDate= new DateTime(2022, 08, 01) } +// }, +// StudentClockInDetails = new List +// { +// new StudentClockInDetail{ LastClockedInDate = new DateTime(2024, 04, 01), LastClockedInPoints = 10 }, +// new StudentClockInDetail{ LastClockedInDate = new DateTime(2024, 04, 02), LastClockedInPoints = 30 } +// } +// }; +// //Template +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @" +//{{ StudentName }} Invoices +//LOOP #1 +//{{ #foreach(invoices) }} +//
{{ Id }} On Loop #1
+//{{ #endforeach }} +//LOOP #2 +//{{ #foreach(studentClockInDetails) }} +//
Got {{ LastClockedInPoints }} for {{ LastClockedInDate:yyyy-MM-dd }}
+//{{ #endforeach }}" +// }; +// string generatedTemplate = template.Map(student); +// string expectedResult = @" +//John Doe Invoices +//LOOP #1 + +//
2 On Loop #1
+ +//
1 On Loop #1
+ +//LOOP #2 + +//
Got 10 for 2024-04-01
+ +//
Got 30 for 2024-04-02
+//"; + +// Assert.Equal(expectedResult, generatedTemplate, false, true, true); +// } + +// [Fact] +// public void Should_Map_Array_Of_String_With_Formatting() +// { +// //Create Model +// Student student = new Student +// { +// ArrayOfString = new string[] +// { +// "String 001", +// "String 002" +// } +// }; +// //Template +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"{{ #foreach(ArrayOfString) }} {{ . }} | {{ .:uppercase }} {{ #endforeach }}" +// }; +// string generatedTemplate = template.Map(student); +// string expectedResult = " String 001 | STRING 001 String 002 | STRING 002 "; +// Assert.Equal(expectedResult, generatedTemplate, false, true, true); +// } + + +// [Fact] +// public void Should_Map_Array_Of_Double_With_Formatting_Test() +// { +// //Create Model +// Student student = new Student +// { +// ArrayOfDouble = new double[] +// { +// 1000.15, +// 2000.22 +// } +// }; +// //Template +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"{{ #foreach(ArrayOfDouble) }} {{ . }} | {{ .:N0 }} {{ #endforeach }}" +// }; +// string generatedTemplate = template.Map(student); +// string expectedResult = " 1000.15 | 1,000 2000.22 | 2,000 "; +// Assert.Equal(expectedResult, generatedTemplate, false, true, true); +// } +// } +//} diff --git a/ObjectSemantics.NET.Tests/IfConditionTests.cs b/ObjectSemantics.NET.Tests/IfConditionTests.cs index dc997a9..489c20d 100644 --- a/ObjectSemantics.NET.Tests/IfConditionTests.cs +++ b/ObjectSemantics.NET.Tests/IfConditionTests.cs @@ -1,117 +1,117 @@ -using ObjectSemantics.NET.Tests.MoqModels; -using System.Collections.Generic; -using Xunit; - -namespace ObjectSemantics.NET.Tests -{ - public class IfConditionTests - { - [Theory] - [InlineData(1, "Valid")] - [InlineData(0, "Invalid")] - public void Should_Render_If_Block_When_Condition_Is_True(int id, string expected) - { - var model = new Invoice { Id = id }; - - var template = new ObjectSemanticsTemplate - { - FileContents = @"{{ #if(Id == 1) }}Valid{{ #else }}Invalid{{ #endif }}" - }; - - string result = template.Map(model); - Assert.Equal(expected, result); - } - - - [Theory] - [InlineData(18, "Minor")] - [InlineData(21, "Adult")] - [InlineData(5, "Minor")] - public void Should_Handle_LessThan_Or_Equal(int age, string expected) - { - var model = new Student { Age = age }; - - var template = new ObjectSemanticsTemplate - { - FileContents = @"{{ #if(Age <= 18) }}Minor{{ #else }}Adult{{ #endif }}" - }; - - var result = template.Map(model); - Assert.Equal(expected, result); - } - - - [Theory] - [InlineData(1, "1")] - [InlineData(0, "Error")] - [InlineData(-1, "Error")] - [InlineData(5, "5")] - [InlineData(+2, "2")] - public void Should_Handle_Whitespace_And_Case_Insensitive_Condition(int id, string expected) - { - var model = new Invoice { Id = id }; - - var template = new ObjectSemanticsTemplate - { - FileContents = @"{{ #if( id > 0 ) }}{{id}}{{ #else }}Error{{ #endif }}" - }; - - var result = template.Map(model); - Assert.Equal(expected, result); - } - - - [Fact] - public void Should_Render_If_Block_Without_Else_When_True() - { - var model = new Student { IsActive = true }; - - var template = new ObjectSemanticsTemplate - { - FileContents = @"Student: John {{ #if(IsActive == true) }}[Is Active]{{ #endif }}" - }; - - var result = template.Map(model); - Assert.Equal("Student: John [Is Active]", result); - } - - [Fact] - public void Should_Evaluate_If_Enumerable_Count() - { - var model = new Student - { - Invoices = new List - { - new Invoice{ Id = 2, RefNo = "INV_002" }, - new Invoice{ Id = 1, RefNo = "INV_001" } - } - }; - - var template = new ObjectSemanticsTemplate - { - FileContents = @"{{ #if(Invoices == 2) }}Matched{{ #else }}Not Matched{{ #endif }}" - }; - - var result = template.Map(model); - Assert.Equal("Matched", result); - } - - [Fact] - public void Should_Evaluate_Empty_Enumerable_As_Zero() - { - var model = new Student - { - Invoices = new List() - }; - - var template = new ObjectSemanticsTemplate - { - FileContents = @"{{ #if(Invoices == 0) }}No invoices available{{ #else }}Invoices Found{{ #endif }}" - }; - - var result = template.Map(model); - Assert.Equal("No invoices available", result); - } - - } -} +//using ObjectSemantics.NET.Tests.MoqModels; +//using System.Collections.Generic; +//using Xunit; + +//namespace ObjectSemantics.NET.Tests +//{ +// public class IfConditionTests +// { +// [Theory] +// [InlineData(1, "Valid")] +// [InlineData(0, "Invalid")] +// public void Should_Render_If_Block_When_Condition_Is_True(int id, string expected) +// { +// var model = new Invoice { Id = id }; + +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"{{ #if(Id == 1) }}Valid{{ #else }}Invalid{{ #endif }}" +// }; + +// string result = template.Map(model); +// Assert.Equal(expected, result); +// } + + +// [Theory] +// [InlineData(18, "Minor")] +// [InlineData(21, "Adult")] +// [InlineData(5, "Minor")] +// public void Should_Handle_LessThan_Or_Equal(int age, string expected) +// { +// var model = new Student { Age = age }; + +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"{{ #if(Age <= 18) }}Minor{{ #else }}Adult{{ #endif }}" +// }; + +// var result = template.Map(model); +// Assert.Equal(expected, result); +// } + + +// [Theory] +// [InlineData(1, "1")] +// [InlineData(0, "Error")] +// [InlineData(-1, "Error")] +// [InlineData(5, "5")] +// [InlineData(+2, "2")] +// public void Should_Handle_Whitespace_And_Case_Insensitive_Condition(int id, string expected) +// { +// var model = new Invoice { Id = id }; + +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"{{ #if( id > 0 ) }}{{id}}{{ #else }}Error{{ #endif }}" +// }; + +// var result = template.Map(model); +// Assert.Equal(expected, result); +// } + + +// [Fact] +// public void Should_Render_If_Block_Without_Else_When_True() +// { +// var model = new Student { IsActive = true }; + +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"Student: John {{ #if(IsActive == true) }}[Is Active]{{ #endif }}" +// }; + +// var result = template.Map(model); +// Assert.Equal("Student: John [Is Active]", result); +// } + +// [Fact] +// public void Should_Evaluate_If_Enumerable_Count() +// { +// var model = new Student +// { +// Invoices = new List +// { +// new Invoice{ Id = 2, RefNo = "INV_002" }, +// new Invoice{ Id = 1, RefNo = "INV_001" } +// } +// }; + +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"{{ #if(Invoices == 2) }}Matched{{ #else }}Not Matched{{ #endif }}" +// }; + +// var result = template.Map(model); +// Assert.Equal("Matched", result); +// } + +// [Fact] +// public void Should_Evaluate_Empty_Enumerable_As_Zero() +// { +// var model = new Student +// { +// Invoices = new List() +// }; + +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"{{ #if(Invoices == 0) }}No invoices available{{ #else }}Invoices Found{{ #endif }}" +// }; + +// var result = template.Map(model); +// Assert.Equal("No invoices available", result); +// } + +// } +//} diff --git a/ObjectSemantics.NET.Tests/MoqFiles/template-example.txt b/ObjectSemantics.NET.Tests/MoqFiles/template-example.txt deleted file mode 100644 index e9b68dc..0000000 --- a/ObjectSemantics.NET.Tests/MoqFiles/template-example.txt +++ /dev/null @@ -1 +0,0 @@ -My Name is: {{ StudentName }} and my balance is: {{ Balance }} \ No newline at end of file diff --git a/ObjectSemantics.NET.Tests/MoqModels/Car.cs b/ObjectSemantics.NET.Tests/MoqModels/Car.cs new file mode 100644 index 0000000..1d734b3 --- /dev/null +++ b/ObjectSemantics.NET.Tests/MoqModels/Car.cs @@ -0,0 +1,23 @@ +using System; + +namespace ObjectSemantics.NET.Tests.MoqModels +{ + public class Car + { + public int Id { get; set; } + public string Make { get; set; } + public int Year { get; set; } + public double EngineSize { get; set; } + public float FuelEfficiency { get; set; } + public decimal Price { get; set; } + public bool IsElectric { get; set; } + public bool? IsLiked { get; set; } = null; + public DateTime ManufactureDate { get; set; } + public DateTime? LastServiceDate { get; set; } + public Guid UniqueId { get; set; } + public char Rating { get; set; } + public byte SafetyScore { get; set; } + public long Mileage { get; set; } + public short NumberOfDoors { get; set; } + } +} diff --git a/ObjectSemantics.NET.Tests/ObjectSemantics.NET.Tests.csproj b/ObjectSemantics.NET.Tests/ObjectSemantics.NET.Tests.csproj index 2641d6f..c804413 100644 --- a/ObjectSemantics.NET.Tests/ObjectSemantics.NET.Tests.csproj +++ b/ObjectSemantics.NET.Tests/ObjectSemantics.NET.Tests.csproj @@ -1,36 +1,30 @@ - - netcoreapp3.1 - - false - - 3.0.1.1 - - 3.0.1.1 - - - - - Always - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - + + netcoreapp3.1 + + false + + 3.0.1.1 + + 3.0.1.1 + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/ObjectSemantics.NET.Tests/PropertyBooleanMapTests.cs b/ObjectSemantics.NET.Tests/PropertyBooleanMapTests.cs new file mode 100644 index 0000000..0ff5e6a --- /dev/null +++ b/ObjectSemantics.NET.Tests/PropertyBooleanMapTests.cs @@ -0,0 +1,35 @@ +using ObjectSemantics.NET.Tests.MoqModels; +using Xunit; + +namespace ObjectSemantics.NET.Tests +{ + public class PropertyBooleanMapTests + { + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Should_Map_From_Bool(bool isElectric) + { + Car car = new Car() + { + IsElectric = isElectric + }; + string result = car.Map("Electric: {{ IsElectric }}"); + Assert.Equal($"Electric: {isElectric}", result, false, true, true); + } + + [Theory] + [InlineData(null)] + [InlineData(true)] + [InlineData(false)] + public void Should_Map_From_Nullable_Bool(bool? isLiked) + { + Car car = new Car() + { + IsLiked = isLiked + }; + string result = car.Map("Liked: {{ IsLiked }}"); + Assert.Equal($"Liked: {isLiked}", result, false, true, true); + } + } +} diff --git a/ObjectSemantics.NET.Tests/PropertyNullableTests.cs b/ObjectSemantics.NET.Tests/PropertyNullableTests.cs index 6cb7998..fc22201 100644 --- a/ObjectSemantics.NET.Tests/PropertyNullableTests.cs +++ b/ObjectSemantics.NET.Tests/PropertyNullableTests.cs @@ -1,68 +1,68 @@ -using ObjectSemantics.NET.Tests.MoqModels; -using System; -using Xunit; +//using ObjectSemantics.NET.Tests.MoqModels; +//using System; +//using Xunit; -namespace ObjectSemantics.NET.Tests -{ - public class PropertyNullableTests - { - [Fact] - public void Should_Map_Nullable_DateTime_Property_Given_NULL() - { - //Create Model - StudentClockInDetail clockInDetails = new StudentClockInDetail { LastClockedInDate = null }; - var template = new ObjectSemanticsTemplate - { - FileContents = @"Last Clocked In: {{ LastClockedInDate:yyyy-MM-dd }}" - }; - string generatedTemplate = template.Map(clockInDetails); - string expectedString = "Last Clocked In: "; - Assert.Equal(expectedString, generatedTemplate, false, true, true); - } +//namespace ObjectSemantics.NET.Tests +//{ +// public class PropertyNullableTests +// { +// [Fact] +// public void Should_Map_Nullable_DateTime_Property_Given_NULL() +// { +// //Create Model +// StudentClockInDetail clockInDetails = new StudentClockInDetail { LastClockedInDate = null }; +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"Last Clocked In: {{ LastClockedInDate:yyyy-MM-dd }}" +// }; +// string generatedTemplate = template.Map(clockInDetails); +// string expectedString = "Last Clocked In: "; +// Assert.Equal(expectedString, generatedTemplate, false, true, true); +// } - [Fact] - public void Should_Map_Nullable_DateTime_Property_Given_A_Value() - { - //Create Model - StudentClockInDetail clockInDetails = new StudentClockInDetail { LastClockedInDate = DateTime.Now }; - var template = new ObjectSemanticsTemplate - { - FileContents = @"Last Clocked In: {{ LastClockedInDate:yyyy-MM-dd }}" - }; - string generatedTemplate = template.Map(clockInDetails); - string expectedString = $"Last Clocked In: {DateTime.Now:yyyy-MM-dd}"; - Assert.Equal(expectedString, generatedTemplate, false, true, true); - } +// [Fact] +// public void Should_Map_Nullable_DateTime_Property_Given_A_Value() +// { +// //Create Model +// StudentClockInDetail clockInDetails = new StudentClockInDetail { LastClockedInDate = DateTime.Now }; +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"Last Clocked In: {{ LastClockedInDate:yyyy-MM-dd }}" +// }; +// string generatedTemplate = template.Map(clockInDetails); +// string expectedString = $"Last Clocked In: {DateTime.Now:yyyy-MM-dd}"; +// Assert.Equal(expectedString, generatedTemplate, false, true, true); +// } - [Fact] - public void Should_Map_Nullable_Number_Property_Given_NULL() - { - //Create Model - StudentClockInDetail clockInDetails = new StudentClockInDetail { LastClockedInPoints = null }; - var template = new ObjectSemanticsTemplate - { - FileContents = @"Last Clocked In Points: {{ LastClockedInPoints:N2 }}" - }; - string generatedTemplate = template.Map(clockInDetails); - string expectedString = "Last Clocked In Points: "; - Assert.Equal(expectedString, generatedTemplate, false, true, true); - } +// [Fact] +// public void Should_Map_Nullable_Number_Property_Given_NULL() +// { +// //Create Model +// StudentClockInDetail clockInDetails = new StudentClockInDetail { LastClockedInPoints = null }; +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"Last Clocked In Points: {{ LastClockedInPoints:N2 }}" +// }; +// string generatedTemplate = template.Map(clockInDetails); +// string expectedString = "Last Clocked In Points: "; +// Assert.Equal(expectedString, generatedTemplate, false, true, true); +// } - [Theory] - [InlineData(null)] - [InlineData(2500)] - [InlineData(200)] - public void Should_Map_Nullable_Number_Property_Given_A_Value(long? number) - { - //Create Model - StudentClockInDetail clockInDetails = new StudentClockInDetail { LastClockedInPoints = number }; - var template = new ObjectSemanticsTemplate - { - FileContents = @"Last Clocked In Points: {{ LastClockedInPoints:N2 }}" - }; - string generatedTemplate = template.Map(clockInDetails); - string expectedString = $"Last Clocked In Points: {number:N2}"; - Assert.Equal(expectedString, generatedTemplate, false, true, true); - } - } -} +// [Theory] +// [InlineData(null)] +// [InlineData(2500)] +// [InlineData(200)] +// public void Should_Map_Nullable_Number_Property_Given_A_Value(long? number) +// { +// //Create Model +// StudentClockInDetail clockInDetails = new StudentClockInDetail { LastClockedInPoints = number }; +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"Last Clocked In Points: {{ LastClockedInPoints:N2 }}" +// }; +// string generatedTemplate = template.Map(clockInDetails); +// string expectedString = $"Last Clocked In Points: {number:N2}"; +// Assert.Equal(expectedString, generatedTemplate, false, true, true); +// } +// } +//} diff --git a/ObjectSemantics.NET.Tests/PropertyNumberMapTests.cs b/ObjectSemantics.NET.Tests/PropertyNumberMapTests.cs new file mode 100644 index 0000000..bd76865 --- /dev/null +++ b/ObjectSemantics.NET.Tests/PropertyNumberMapTests.cs @@ -0,0 +1,74 @@ +using ObjectSemantics.NET.Tests.MoqModels; +using Xunit; + +namespace ObjectSemantics.NET.Tests +{ + public class PropertyNumberMapTests + { + [Theory] + [InlineData(2013)] + [InlineData(2025000)] + public void Should_Map_From_Int(int year) + { + Car car = new Car() + { + Year = year + }; + string result = car.Map("Year: {{ Year }}"); + Assert.Equal($"Year: {year}", result); + } + + [Theory] + [InlineData(3.5)] + [InlineData(1.3)] + public void Should_Map_From_Double(double size) + { + Car car = new Car() + { + EngineSize = size + }; + string result = car.Map("Engine size: {{ EngineSize }}"); + Assert.Equal($"Engine size: {size}", result); + } + + [Theory] + [InlineData(14.7f)] + [InlineData(10.2f)] + public void Should_Map_From_Float(float efficiency) + { + Car car = new Car() + { + FuelEfficiency = efficiency + }; + string result = car.Map("Fuel efficiency: {{ FuelEfficiency }}"); + Assert.Equal($"Fuel efficiency: {efficiency}", result); + } + + [Theory] + [InlineData(1050000.75)] + [InlineData(800.25)] + public void Should_Map_From_Decimal(decimal price) + { + Car car = new Car() + { + Price = price + }; + string result = car.Map("Price: {{ Price }}"); + Assert.Equal($"Price: {price}", result); + } + + [Theory] + [InlineData(20000)] + [InlineData(50_000)] + [InlineData(100000)] + public void Should_Support_Number_To_String_Formatting(decimal price) + { + Car car = new Car + { + Price = price + }; + string generatedTemplate = car.Map("{{ Price:#,##0 }}|{{ Price:N5 }}"); + Assert.Equal($"{price:#,##0}|{price:N5}", generatedTemplate); + } + } +} diff --git a/ObjectSemantics.NET.Tests/PropertyStringMapTests.cs b/ObjectSemantics.NET.Tests/PropertyStringMapTests.cs new file mode 100644 index 0000000..ec298b7 --- /dev/null +++ b/ObjectSemantics.NET.Tests/PropertyStringMapTests.cs @@ -0,0 +1,84 @@ +using ObjectSemantics.NET.Tests.MoqModels; +using Xunit; + +namespace ObjectSemantics.NET.Tests +{ + public class PropertyStringMapTests + { + [Theory] + [InlineData("BMW")] + [InlineData("Toyota")] + [InlineData("Mercedes-Benz")] + [InlineData("Land Rover")] + public void Should_Map_From_Simple_String(string make) + { + Car car = new Car + { + Make = make + }; + string generatedTemplate = car.Map("My car make is {{ Make }}."); + Assert.Equal($"My car make is {make}.", generatedTemplate); + } + + [Theory] + [InlineData("L(rover")] + [InlineData("Toy >> tA")] + [InlineData("Peugeot 308")] + [InlineData("Fiat 500X")] + public void Should_Map_From_SpecialChar_String(string make) + { + Car car = new Car + { + Make = make + }; + string generatedTemplate = car.Map("My car make is {{ Make }}."); + Assert.Equal($"My car make is {make}.", generatedTemplate); + } + + [Fact] + public void Should_Map_From_Null_String() + { + Car car = new Car + { + Make = null + }; + string generatedTemplate = car.Map("My car make is {{ Make }}."); + Assert.Equal($"My car make is {car.Make}.", generatedTemplate); + } + + public void Should_Format_String_To_UpperCase() + { + Car car = new Car + { + Make = "Toyota" + }; + string generatedTemplate = car.Map("{{ Make:uppercase }}"); + Assert.Equal(car.Make.ToUpper(), generatedTemplate); + } + + [Fact] + public void Should_Format_String_To_LowerCase() + { + Car car = new Car + { + Make = "ToYota" + }; + string generatedTemplate = car.Map("{{ Make:lowercase }}"); + Assert.Equal(car.Make.ToLower(), generatedTemplate); + } + + [Theory] + [InlineData("toYota", "Toyota")] + [InlineData("BMW", "Bmw")] + [InlineData("Peugeot 308", "Peugeot 308")] + public void Should_Format_String_To_TitleCase(string make, string expectedTitleCase) + { + Car car = new Car + { + Make = make + }; + string generatedTemplate = car.Map("{{ Make:titlecase }}"); + Assert.Equal(expectedTitleCase, generatedTemplate); + } + } +} diff --git a/ObjectSemantics.NET.Tests/StringFormattingTests.cs b/ObjectSemantics.NET.Tests/StringFormattingTests.cs index 07354f4..acceb10 100644 --- a/ObjectSemantics.NET.Tests/StringFormattingTests.cs +++ b/ObjectSemantics.NET.Tests/StringFormattingTests.cs @@ -1,169 +1,123 @@ -using ObjectSemantics.NET.Tests.MoqModels; -using System; -using Xunit; - -namespace ObjectSemantics.NET.Tests -{ - public class StringFormattingTests - { - [Fact] - public void Should_Accept_String_To_UpperCase_or_LowerCase_Formatting() - { - //Create Model - Student student = new Student - { - StudentName = "WILLiaM" - }; - //Template - var template = new ObjectSemanticsTemplate - { - FileContents = @"Original StudentName: {{ StudentName }}, Uppercase StudentName: {{ StudentName:UPPERCASE }}, Lowercase StudentName: {{ StudentName:lowercase }}" - }; - string generatedTemplate = template.Map(student); - string expectedString = "Original StudentName: WILLiaM, Uppercase StudentName: WILLIAM, Lowercase StudentName: william"; - Assert.Equal(expectedString, generatedTemplate, false, true, true); - } - - [Theory] - [InlineData("john doe", "John Doe")] - [InlineData("JANE DOE", "Jane Doe")] - [InlineData("aLiCe joHNsOn", "Alice Johnson")] - [InlineData("", "")] - [InlineData(null, "")] - public void Should_Convert_StudentName_To_TitleCase(string studentName, string expectedTitleCase) - { - // Create Model - Student student = new Student - { - StudentName = studentName - }; - - // Template - var template = new ObjectSemanticsTemplate - { - FileContents = @"{{ StudentName:titlecase }}" - }; - - string generatedTemplate = template.Map(student); - - Assert.Equal(expectedTitleCase, generatedTemplate, ignoreCase: false, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); - } - - - [Fact] - public void Should_Accept_Number_To_String_Formatting() - { - //Create Model - Student student = new Student - { - Balance = 20000.5788 - }; - //Template - var template = new ObjectSemanticsTemplate - { - FileContents = @"Original Balance: {{ Balance }}, #,##0 Balance: {{ Balance:#,##0 }}, N5 Balance: {{ Balance:N5 }}" - }; - string generatedTemplate = template.Map(student); - string expectedString = "Original Balance: 20000.5788, #,##0 Balance: 20,001, N5 Balance: 20,000.57880"; - Assert.Equal(expectedString, generatedTemplate, false, true, true); - } - - [Fact] - public void Should_Accept_DateTime_To_String_Formatting() - { - //Lets see how it can handle multiple : {{ RegDate:yyyy-MM-dd HH:mm tt }} - Student student = new Student - { - RegDate = new DateTime(2022, 11, 27, 18, 13, 59) - }; - //Template - var template = new ObjectSemanticsTemplate - { - FileContents = @"Original RegDate: {{ RegDate }}, yyyy RegDate: {{ RegDate:yyyy }}, yyyy-MM-dd HH:mm tt RegDate: {{ RegDate:yyyy-MM-dd HH:mm tt }}" - }; - string generatedTemplate = template.Map(student); - string expectedString = "Original RegDate: 11/27/2022 6:13:59 PM, yyyy RegDate: 2022, yyyy-MM-dd HH:mm tt RegDate: 2022-11-27 18:13 PM"; - Assert.Equal(expectedString, generatedTemplate, false, true, true); - } - - - [Fact] - public void Should_Accept_String_To_MD5_Formatting() - { - //Create Model - Student student = new Student - { - StudentName = "John DOE" - }; - //Template - var template = new ObjectSemanticsTemplate - { - FileContents = @"Original String: {{ StudentName }} | To MD5 String: {{ StudentName:ToMD5 }}" - }; - string generatedTemplate = template.Map(student); - string expectedString = "Original String: John DOE | To MD5 String: 82AF64057A5F0D528CEE6F55D05823D7"; - Assert.Equal(expectedString, generatedTemplate, false, true, true); - } - - - [Fact] - public void Should_Accept_String_To_BASE64_Formatting() - { - //Create Model - Student student = new Student - { - StudentName = "John DOE" - }; - //Template - var template = new ObjectSemanticsTemplate - { - FileContents = @"Original String: {{ StudentName }} | To BASE64 String: {{ StudentName:ToBase64 }}" - }; - string generatedTemplate = template.Map(student); - string expectedString = "Original String: John DOE | To BASE64 String: Sm9obiBET0U="; - Assert.Equal(expectedString, generatedTemplate, false, true, true); - } - - [Fact] - public void Should_Accept_String_From_BASE64_Formatting() - { - //Create Model - Student student = new Student - { - StudentName = "Sm9obiBET0U=" - }; - //Template - var template = new ObjectSemanticsTemplate - { - FileContents = @"Original String: {{ StudentName }} | From BASE64 String: {{ StudentName:FromBase64 }}" - }; - string generatedTemplate = template.Map(student); - string expectedString = "Original String: Sm9obiBET0U= | From BASE64 String: John DOE"; - Assert.Equal(expectedString, generatedTemplate, false, true, true); - } - - [Theory] - [InlineData("Alice Johnson", "13")] - [InlineData("Bob", "3")] - [InlineData("", "0")] - [InlineData(null, "")] - public void Should_Return_Correct_Length_Of_StudentName(string studentName, string expectedLength) - { - // Create Model - Student student = new Student - { - StudentName = studentName - }; - - // Template - var template = new ObjectSemanticsTemplate - { - FileContents = @"{{ StudentName:length }}" - }; - - string generatedTemplate = template.Map(student); - - Assert.Equal(expectedLength, generatedTemplate, ignoreCase: false, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); - } - } -} +//using ObjectSemantics.NET.Tests.MoqModels; +//using System; +//using Xunit; + + + +// [Fact] +// public void Should_Accept_Number_To_String_Formatting() +// { +// //Create Model +// Student student = new Student +// { +// Balance = 20000.5788 +// }; +// //Template +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"Original Balance: {{ Balance }}, #,##0 Balance: {{ Balance:#,##0 }}, N5 Balance: {{ Balance:N5 }}" +// }; +// string generatedTemplate = template.Map(student); +// string expectedString = "Original Balance: 20000.5788, #,##0 Balance: 20,001, N5 Balance: 20,000.57880"; +// Assert.Equal(expectedString, generatedTemplate, false, true, true); +// } + +// [Fact] +// public void Should_Accept_DateTime_To_String_Formatting() +// { +// //Lets see how it can handle multiple : {{ RegDate:yyyy-MM-dd HH:mm tt }} +// Student student = new Student +// { +// RegDate = new DateTime(2022, 11, 27, 18, 13, 59) +// }; +// //Template +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"Original RegDate: {{ RegDate }}, yyyy RegDate: {{ RegDate:yyyy }}, yyyy-MM-dd HH:mm tt RegDate: {{ RegDate:yyyy-MM-dd HH:mm tt }}" +// }; +// string generatedTemplate = template.Map(student); +// string expectedString = "Original RegDate: 11/27/2022 6:13:59 PM, yyyy RegDate: 2022, yyyy-MM-dd HH:mm tt RegDate: 2022-11-27 18:13 PM"; +// Assert.Equal(expectedString, generatedTemplate, false, true, true); +// } + + +// [Fact] +// public void Should_Accept_String_To_MD5_Formatting() +// { +// //Create Model +// Student student = new Student +// { +// StudentName = "John DOE" +// }; +// //Template +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"Original String: {{ StudentName }} | To MD5 String: {{ StudentName:ToMD5 }}" +// }; +// string generatedTemplate = template.Map(student); +// string expectedString = "Original String: John DOE | To MD5 String: 82AF64057A5F0D528CEE6F55D05823D7"; +// Assert.Equal(expectedString, generatedTemplate, false, true, true); +// } + + +// [Fact] +// public void Should_Accept_String_To_BASE64_Formatting() +// { +// //Create Model +// Student student = new Student +// { +// StudentName = "John DOE" +// }; +// //Template +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"Original String: {{ StudentName }} | To BASE64 String: {{ StudentName:ToBase64 }}" +// }; +// string generatedTemplate = template.Map(student); +// string expectedString = "Original String: John DOE | To BASE64 String: Sm9obiBET0U="; +// Assert.Equal(expectedString, generatedTemplate, false, true, true); +// } + +// [Fact] +// public void Should_Accept_String_From_BASE64_Formatting() +// { +// //Create Model +// Student student = new Student +// { +// StudentName = "Sm9obiBET0U=" +// }; +// //Template +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"Original String: {{ StudentName }} | From BASE64 String: {{ StudentName:FromBase64 }}" +// }; +// string generatedTemplate = template.Map(student); +// string expectedString = "Original String: Sm9obiBET0U= | From BASE64 String: John DOE"; +// Assert.Equal(expectedString, generatedTemplate, false, true, true); +// } + +// [Theory] +// [InlineData("Alice Johnson", "13")] +// [InlineData("Bob", "3")] +// [InlineData("", "0")] +// [InlineData(null, "")] +// public void Should_Return_Correct_Length_Of_StudentName(string studentName, string expectedLength) +// { +// // Create Model +// Student student = new Student +// { +// StudentName = studentName +// }; + +// // Template +// var template = new ObjectSemanticsTemplate +// { +// FileContents = @"{{ StudentName:length }}" +// }; + +// string generatedTemplate = template.Map(student); + +// Assert.Equal(expectedLength, generatedTemplate, ignoreCase: false, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); +// } +// } +//} diff --git a/ObjectSemantics.NET/Engine/EngineAlgorithim.cs b/ObjectSemantics.NET/Engine/EngineAlgorithim.cs index d4fb9ed..1db10ee 100644 --- a/ObjectSemantics.NET/Engine/EngineAlgorithim.cs +++ b/ObjectSemantics.NET/Engine/EngineAlgorithim.cs @@ -21,7 +21,7 @@ internal static class EngineAlgorithim private static readonly Regex DirectParamRegex = new Regex(@"{{(.+?)}}", RegexOptions.IgnoreCase | RegexOptions.Compiled); - public static string GenerateFromTemplate(T record, EngineRunnerTemplate template, List parameterKeyValues = null, TemplateMapperOptions options = null) where T : new() + public static string GenerateFromTemplate(T record, EngineRunnerTemplate template, Dictionary parameterKeyValues = null, TemplateMapperOptions options = null) where T : new() { List objProperties = GetObjectProperties(record, parameterKeyValues); Dictionary propMap = objProperties.ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase); @@ -86,14 +86,12 @@ internal static class EngineAlgorithim Type = row.GetType(), OriginalValue = row }; - activeRow.Replace(objLoopCode.ReplaceRef, - tempProp.GetPropertyDisplayString(objLoopCode.GetFormattingCommand(), options)); + activeRow.Replace(objLoopCode.ReplaceRef, tempProp.GetPropertyDisplayString(objLoopCode.GetFormattingCommand(), options)); } else { if (rowMap.TryGetValue(objLoopCode.TargetPropertyName, out ExtractedObjProperty p)) - activeRow.Replace(objLoopCode.ReplaceRef, - p.GetPropertyDisplayString(objLoopCode.GetFormattingCommand(), options)); + activeRow.Replace(objLoopCode.ReplaceRef, p.GetPropertyDisplayString(objLoopCode.GetFormattingCommand(), options)); else activeRow.Replace(objLoopCode.ReplaceRef, objLoopCode.ReplaceCommand); } @@ -185,7 +183,7 @@ internal static EngineRunnerTemplate GenerateRunnerTemplate(string fileContent) return templatedContent; } - private static List GetObjectProperties(T value, List parameters) where T : new() + private static List GetObjectProperties(T value, Dictionary parameters) where T : new() { Type type = typeof(T); if (!PropertyCache.TryGetValue(type, out PropertyInfo[] props)) @@ -208,7 +206,7 @@ internal static EngineRunnerTemplate GenerateRunnerTemplate(string fileContent) if (parameters != null) { - foreach (ObjectSemanticsKeyValue p in parameters) + foreach (KeyValuePair p in parameters) result.Add(new ExtractedObjProperty { Type = p.Value.GetType(), Name = p.Key, OriginalValue = p.Value }); } diff --git a/ObjectSemantics.NET/ObjectSemantics.NET.csproj b/ObjectSemantics.NET/ObjectSemantics.NET.csproj index 2f1edc7..31642f6 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 - 6.0.5 - 6.0.5 - 6.0.5 + 7.0.0 + 7.0.0 + 7.0.0 false README.md diff --git a/ObjectSemantics.NET/ObjectSemanticsKeyValue.cs b/ObjectSemantics.NET/ObjectSemanticsKeyValue.cs deleted file mode 100644 index 9507d1d..0000000 --- a/ObjectSemantics.NET/ObjectSemanticsKeyValue.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace ObjectSemantics.NET -{ - public class ObjectSemanticsKeyValue - { - public ObjectSemanticsKeyValue() { } - public ObjectSemanticsKeyValue(string key, object value) - { - Key = key; - Value = value; - } - - public string Key { get; set; } - public object Value { get; set; } - } -} diff --git a/ObjectSemantics.NET/ObjectSemanticsTemplate.cs b/ObjectSemantics.NET/ObjectSemanticsTemplate.cs deleted file mode 100644 index a97c68d..0000000 --- a/ObjectSemantics.NET/ObjectSemanticsTemplate.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace ObjectSemantics.NET -{ - public class ObjectSemanticsTemplate - { - public string Name { get; set; } = "default.template"; - public string FileContents { get; set; } - public string Author { get; set; } - public DateTime CreatedDate { get; set; } = DateTime.Now; - } -} \ No newline at end of file diff --git a/ObjectSemantics.NET/TemplateMapper.cs b/ObjectSemantics.NET/TemplateMapper.cs index 6ad839b..80b7b40 100644 --- a/ObjectSemantics.NET/TemplateMapper.cs +++ b/ObjectSemantics.NET/TemplateMapper.cs @@ -8,34 +8,36 @@ namespace ObjectSemantics.NET public static class TemplateMapper { /// - /// Generate a Data Template From Object Properties + /// Generate a mapped string from string /// /// - /// Single Record of T that may include a Collection inside it - /// Additional Key Value parameters that you may need mapped to file - /// Custom Options and configurations for the Template Generator + /// + /// + /// + /// /// - public static string Map(this ObjectSemanticsTemplate template, T record, List additionalKeyValues = null, TemplateMapperOptions options = null) where T : new() + public static string Map(this string template, T record, Dictionary additionalKeyValues = null, TemplateMapperOptions options = null) where T : new() { return Map(record, template, additionalKeyValues, options); } /// - /// Generate a Data Template From Object Properties + /// Generates a mapped string from a T record and a template /// /// - /// Single Record of T that may include a Collection inside it - /// Template File containing Template Name and Template Contents - /// Additional Key Value parameters that you may need mapped to file - /// Custom Options and configurations for the Template Generator + /// + /// + /// + /// /// - public static string Map(T record, ObjectSemanticsTemplate template, List additionalKeyValues = null, TemplateMapperOptions options = null) where T : new() + /// + public static string Map(this T record, string template, Dictionary additionalKeyValues = null, TemplateMapperOptions options = null) where T : new() { if (record == null) return string.Empty; if (template == null) throw new Exception("Template Object can't be NULL"); if (options == null) options = new TemplateMapperOptions(); - EngineRunnerTemplate runnerTemplate = EngineAlgorithim.GenerateRunnerTemplate(template.FileContents); - return runnerTemplate == null ? throw new Exception($"Error Generating template from specified Template Name: {template.Name}") : EngineAlgorithim.GenerateFromTemplate(record, runnerTemplate, additionalKeyValues, options); + EngineRunnerTemplate runnerTemplate = EngineAlgorithim.GenerateRunnerTemplate(template); + return runnerTemplate == null ? throw new Exception($"Error Mapping!") : EngineAlgorithim.GenerateFromTemplate(record, runnerTemplate, additionalKeyValues, options); } } } \ No newline at end of file From 37ebab7842c7da5fbe3a25560b814b651cec6fd4 Mon Sep 17 00:00:00 2001 From: "George Njeri (Swagfin)" Date: Tue, 7 Oct 2025 00:25:14 +0300 Subject: [PATCH 04/12] chore: extra more unit tests --- .../BasicMappingTests.cs | 85 ------------ .../CharacterEscapingTests.cs | 52 -------- .../CognitiveMapTests.cs | 105 +++++++++++++++ ObjectSemantics.NET.Tests/MoqModels/Car.cs | 3 - ObjectSemantics.NET.Tests/MoqModels/Person.cs | 8 ++ .../PropertyDateTimeMapTests.cs | 43 ++++++ .../PropertyNumberMapTests.cs | 27 ++++ .../PropertyStringMapTests.cs | 56 ++++++++ .../StringFormattingTests.cs | 123 ------------------ 9 files changed, 239 insertions(+), 263 deletions(-) delete mode 100644 ObjectSemantics.NET.Tests/BasicMappingTests.cs delete mode 100644 ObjectSemantics.NET.Tests/CharacterEscapingTests.cs create mode 100644 ObjectSemantics.NET.Tests/CognitiveMapTests.cs create mode 100644 ObjectSemantics.NET.Tests/MoqModels/Person.cs create mode 100644 ObjectSemantics.NET.Tests/PropertyDateTimeMapTests.cs delete mode 100644 ObjectSemantics.NET.Tests/StringFormattingTests.cs diff --git a/ObjectSemantics.NET.Tests/BasicMappingTests.cs b/ObjectSemantics.NET.Tests/BasicMappingTests.cs deleted file mode 100644 index efc6403..0000000 --- a/ObjectSemantics.NET.Tests/BasicMappingTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -//using ObjectSemantics.NET.Tests.MoqModels; -//using System.Collections.Generic; -//using Xunit; - -//namespace ObjectSemantics.NET.Tests -//{ -// public class BasicMappingTests -// { -// [Fact] -// public void Should_Map_Object_To_Template_From_TemplateObject() -// { -// //Create Model -// Student student = new Student -// { -// StudentName = "George Waynne", -// Balance = 2510 -// }; - -// string generatedTemplate = student.Map(@"My Name is: {{ StudentName }}"); -// string expectedString = "My Name is: George Waynne"; -// Assert.Equal(expectedString, generatedTemplate, false, true, true); -// } - - -// [Fact] -// public void Should_Map_Additional_Parameters() -// { -// //Create Model -// Student student = new Student -// { -// StudentName = "George Waynne" -// }; - -// //Additional Parameters -// Dictionary additionalParams = new Dictionary -// { -// { "CompanyName","TEST INC."}, -// { "CompanyEmail","test.inc@test.com"}, -// { "Employees", 1289 } -// }; -// string generatedTemplate = student.Map(@"My Name is: {{ StudentName }} | CompanyName: {{ CompanyName }} | CompanyEmail: {{ CompanyEmail }} | Employees: {{ Employees }}", additionalParams); -// string expectedString = "My Name is: George Waynne | CompanyName: TEST INC. | CompanyEmail: test.inc@test.com | Employees: 1289"; -// Assert.Equal(expectedString, generatedTemplate, false, true, true); -// } - - -// [Fact] -// public void Should_Return_Unknown_Properties_If_Not_Found_In_Object() -// { -// //Create Model -// Student student = new Student -// { -// StudentName = "George Waynne" -// }; -// //Template -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"Unknown Object example: {{StudentIdentityCardXyx}}" -// }; -// string generatedTemplate = template.Map(student); -// string expectedString = "Unknown Object example: {{ StudentIdentityCardXyx }}"; -// Assert.Equal(expectedString, generatedTemplate, false, true, true); -// } - - -// [Fact] -// public void Should_Ignore_Whitespaces_Inside_CurlyBrackets() -// { -// //Create Model -// Student student = new Student -// { -// StudentName = "George Waynne" -// }; -// //Template -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"StudentName is: {{ StudentName }}" -// }; -// string generatedTemplate = template.Map(student); -// string expectedString = "StudentName is: George Waynne"; -// Assert.Equal(expectedString, generatedTemplate, false, true, true); -// } - -// } -//} diff --git a/ObjectSemantics.NET.Tests/CharacterEscapingTests.cs b/ObjectSemantics.NET.Tests/CharacterEscapingTests.cs deleted file mode 100644 index 9008d5c..0000000 --- a/ObjectSemantics.NET.Tests/CharacterEscapingTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -//using ObjectSemantics.NET.Tests.MoqModels; -//using System.Collections.Generic; -//using Xunit; - -//namespace ObjectSemantics.NET.Tests -//{ -// public class CharacterEscapingTests -// { - -// [Fact] -// public void Should_Escape_Xml_Char_If_Option_Is_Enabled() -// { -// //Create Model -// Student student = new Student { StudentName = "I've got \"special\" < & also >" }; -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"{{ StudentName }}" -// }; -// string generatedTemplate = template.Map(student, null, new TemplateMapperOptions -// { -// XmlCharEscaping = true -// }); -// string expectedString = "I've got "special" < & also >"; -// Assert.Equal(expectedString, generatedTemplate, false, true, true); -// } - -// [Fact] -// public void Should_Escape_Xml_Char_In_LOOPS_If_Option_Is_Enabled() -// { -// //Create Model -// Student student = new Student -// { -// Invoices = new List -// { -// new Invoice{ Narration="I've got \"special\""}, -// new Invoice{ Narration="I've got < & also >"} -// } -// }; -// //Template -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"{{ #foreach(invoices) }} [{{ Narration }}] {{ #endforeach }}" -// }; -// string generatedTemplate = template.Map(student, null, new TemplateMapperOptions -// { -// XmlCharEscaping = true -// }); -// string expectedResult = @" [I've got "special"] [I've got < & also >] "; -// Assert.Equal(expectedResult, generatedTemplate, false, true, true); -// } -// } -//} diff --git a/ObjectSemantics.NET.Tests/CognitiveMapTests.cs b/ObjectSemantics.NET.Tests/CognitiveMapTests.cs new file mode 100644 index 0000000..5c9bf56 --- /dev/null +++ b/ObjectSemantics.NET.Tests/CognitiveMapTests.cs @@ -0,0 +1,105 @@ +using ObjectSemantics.NET.Tests.MoqModels; +using System; +using System.Collections.Generic; +using Xunit; + +namespace ObjectSemantics.NET.Tests +{ + public class CognitiveMapTests + { + [Theory] + [InlineData("John Doe")] + [InlineData("Jane Doe")] + public void Library_Entry_Point_T_Extension_Should_Work(string personName) + { + Person person = new Person + { + Name = personName + }; + string generatedTemplate = person.Map("I am {{ Name }}!"); + Assert.Equal($"I am {personName}!", generatedTemplate); + } + + [Theory] + [InlineData("John Doe")] + [InlineData("Jane Doe")] + public void Library_Entry_Point_String_Extension_Should_Work(string personName) + { + Person person = new Person + { + Name = personName + }; + string generatedTemplate = "I am {{ Name }}!".Map(person); + Assert.Equal($"I am {personName}!", generatedTemplate); + } + + [Fact] + public void Additional_Headers_Should_Also_Be_Mapped() + { + Person person = new Person + { + Name = "John Doe" + }; + + //additional params (outside the class) + Dictionary additionalParams = new Dictionary + { + { "Occupation","Developer"}, + { "DateOfBirth", new DateTime(1995, 01, 01) } + }; + + string generatedTemplate = "Name: {{ Name }} | Occupation: {{ Occupation }} | DOB: {{ DateOfBirth }}".Map(person, additionalParams); + + Assert.Equal($"Name: {person.Name} | Occupation: {additionalParams["Occupation"]} | DOB: {additionalParams["DateOfBirth"]}", generatedTemplate); + } + + + [Theory] + [InlineData("I am {{ Name }}")] + [InlineData("I am {{ Name }}")] + [InlineData("I am {{ Name }}")] + [InlineData("I am {{Name}}")] + [InlineData("I am {{ Name }}")] + public void Whitespaces_Inside_Curl_Braces_Should_Be_Ignored(string template) + { + Person person = new Person + { + Name = "John Doe" + }; + string generatedTemplate = person.Map(template); + Assert.Equal($"I am John Doe", generatedTemplate); + } + + [Theory] + [InlineData("{{ MissingPropX }}", "{{ MissingPropX }}")] + [InlineData("{{ MissingProp123 }}", "{{ MissingProp123 }}")] + [InlineData("{{ UnknownPropertyXyz }}", "{{ UnknownPropertyXyz }}")] //it trims the excess whitespaces + [InlineData("{{ Unknown3x}}", "{{ Unknown3x }}")] //it trims the excess whitespaces + public void None_Existing_Properties_Should_Be_Returned_If_Not_Mapped(string template, string expected) + { + Person person = new Person + { + Name = "John Doe" + }; + string generatedTemplate = person.Map(template); + Assert.Equal(expected, generatedTemplate); + } + + + [Theory] + [InlineData("I've got \"special\" < & also >", "I've got "special" < & also >")] + [InlineData("I've got < & also >", "I've got < & also >")] + public void Should_Escape_Xml_Char_Values_If_Option_Is_Enabled(string value, string expected) + { + Person person = new Person() + { + Name = value + }; + string generatedTemplate = person.Map("{{ Name }}", null, new TemplateMapperOptions + { + XmlCharEscaping = true + }); + Assert.Equal(expected, generatedTemplate); + } + } +} diff --git a/ObjectSemantics.NET.Tests/MoqModels/Car.cs b/ObjectSemantics.NET.Tests/MoqModels/Car.cs index 1d734b3..1df589f 100644 --- a/ObjectSemantics.NET.Tests/MoqModels/Car.cs +++ b/ObjectSemantics.NET.Tests/MoqModels/Car.cs @@ -14,9 +14,6 @@ public class Car public bool? IsLiked { get; set; } = null; public DateTime ManufactureDate { get; set; } public DateTime? LastServiceDate { get; set; } - public Guid UniqueId { get; set; } - public char Rating { get; set; } - public byte SafetyScore { get; set; } public long Mileage { get; set; } public short NumberOfDoors { get; set; } } diff --git a/ObjectSemantics.NET.Tests/MoqModels/Person.cs b/ObjectSemantics.NET.Tests/MoqModels/Person.cs new file mode 100644 index 0000000..76bfa43 --- /dev/null +++ b/ObjectSemantics.NET.Tests/MoqModels/Person.cs @@ -0,0 +1,8 @@ +namespace ObjectSemantics.NET.Tests.MoqModels +{ + public class Person + { + public string Name { get; set; } + public int Age { get; set; } + } +} diff --git a/ObjectSemantics.NET.Tests/PropertyDateTimeMapTests.cs b/ObjectSemantics.NET.Tests/PropertyDateTimeMapTests.cs new file mode 100644 index 0000000..730a68a --- /dev/null +++ b/ObjectSemantics.NET.Tests/PropertyDateTimeMapTests.cs @@ -0,0 +1,43 @@ +using ObjectSemantics.NET.Tests.MoqModels; +using System; +using Xunit; + +namespace ObjectSemantics.NET.Tests +{ + public class PropertyDateTimeMapTests + { + [Fact] + public void Should_Map_From_Date() + { + DateTime date = new DateTime(2022, 11, 27, 18, 13, 59); + Car car = new Car() + { + ManufactureDate = date + }; + string result = car.Map("{{ ManufactureDate:yyyy }}|{{ ManufactureDate:yyyy-MM-dd HH:mm tt }}"); + Assert.Equal($"{car.ManufactureDate:yyyy}|{car.ManufactureDate:yyyy-MM-dd HH:mm tt}", result, false, true, true); + } + + [Fact] + public void Should_Map_From_Null_Date() + { + Car car = new Car() + { + LastServiceDate = null + }; + string result = car.Map("Last serviced: {{ LastServiceDate }}"); + Assert.Equal($"Last serviced: {car.LastServiceDate}", result, false, true, true); + } + + [Fact] + public void Should_Map_From_Nullable_Date() + { + Car car = new Car() + { + LastServiceDate = new DateTime(2025, 1, 1) + }; + string result = car.Map("Last serviced: {{ LastServiceDate }}"); + Assert.Equal($"Last serviced: {car.LastServiceDate}", result, false, true, true); + } + } +} diff --git a/ObjectSemantics.NET.Tests/PropertyNumberMapTests.cs b/ObjectSemantics.NET.Tests/PropertyNumberMapTests.cs index bd76865..f08fa1f 100644 --- a/ObjectSemantics.NET.Tests/PropertyNumberMapTests.cs +++ b/ObjectSemantics.NET.Tests/PropertyNumberMapTests.cs @@ -57,6 +57,33 @@ public void Should_Map_From_Decimal(decimal price) Assert.Equal($"Price: {price}", result); } + [Theory] + [InlineData(150000L)] + [InlineData(6000L)] + public void Should_Map_From_Long(long mileage) + { + Car car = new Car() + { + Mileage = mileage + }; + string result = car.Map("Mileage: {{ Mileage }}"); + Assert.Equal($"Mileage: {mileage}", result, false, true, true); + } + + [Theory] + [InlineData((short)4)] + [InlineData((short)2)] + public void Should_Map_From_Short(short doors) + { + Car car = new Car() + { + NumberOfDoors = doors + }; + string result = car.Map("Doors: {{ NumberOfDoors }}"); + Assert.Equal($"Doors: {doors}", result, false, true, true); + } + + [Theory] [InlineData(20000)] [InlineData(50_000)] diff --git a/ObjectSemantics.NET.Tests/PropertyStringMapTests.cs b/ObjectSemantics.NET.Tests/PropertyStringMapTests.cs index ec298b7..90ef124 100644 --- a/ObjectSemantics.NET.Tests/PropertyStringMapTests.cs +++ b/ObjectSemantics.NET.Tests/PropertyStringMapTests.cs @@ -46,6 +46,7 @@ public void Should_Map_From_Null_String() Assert.Equal($"My car make is {car.Make}.", generatedTemplate); } + [Fact] public void Should_Format_String_To_UpperCase() { Car car = new Car @@ -80,5 +81,60 @@ public void Should_Format_String_To_TitleCase(string make, string expectedTitleC string generatedTemplate = car.Map("{{ Make:titlecase }}"); Assert.Equal(expectedTitleCase, generatedTemplate); } + + [Theory] + [InlineData("BMW", "BCB48DDDFF8C14B5F452EE573B4DB770")] + [InlineData("Mercedes-Benz", "BE956E74642D5B18C6A4864AAD39F494")] + [InlineData("Peugeot 308", "8C617F9B2E4DE166E277FEE781D32DAF")] + public void Should_Format_String_To_MD5(string make, string expectedString) + { + Car car = new Car + { + Make = make + }; + string generatedTemplate = car.Map("{{ Make:ToMD5 }}"); + Assert.Equal(expectedString, generatedTemplate); + } + + [Theory] + [InlineData("BMW", "Qk1X")] + [InlineData("Mercedes-Benz", "TWVyY2VkZXMtQmVueg==")] + [InlineData("Peugeot 308", "UGV1Z2VvdCAzMDg=")] + public void Should_Format_String_To_Base64_Format(string make, string expectedString) + { + Car car = new Car + { + Make = make + }; + string generatedTemplate = car.Map("{{ Make:ToBase64 }}"); + Assert.Equal(expectedString, generatedTemplate); + } + + [Theory] + [InlineData("Qk1X", "BMW")] + [InlineData("TWVyY2VkZXMtQmVueg==", "Mercedes-Benz")] + [InlineData("UGV1Z2VvdCAzMDg=", "Peugeot 308")] + public void Should_Format_String_From_Base64_Format(string make, string expectedString) + { + Car car = new Car + { + Make = make + }; + string generatedTemplate = car.Map("{{ Make:FromBase64 }}"); + Assert.Equal(expectedString, generatedTemplate); + } + + [Theory] + [InlineData("BMW")] + [InlineData("Mercedes-Benz")] + public void Should_Accept_Length_String_Format(string make) + { + Car car = new Car + { + Make = make + }; + string generatedTemplate = car.Map("{{ Make:length }}"); + Assert.Equal(make.Length.ToString(), generatedTemplate); + } } } diff --git a/ObjectSemantics.NET.Tests/StringFormattingTests.cs b/ObjectSemantics.NET.Tests/StringFormattingTests.cs deleted file mode 100644 index acceb10..0000000 --- a/ObjectSemantics.NET.Tests/StringFormattingTests.cs +++ /dev/null @@ -1,123 +0,0 @@ -//using ObjectSemantics.NET.Tests.MoqModels; -//using System; -//using Xunit; - - - -// [Fact] -// public void Should_Accept_Number_To_String_Formatting() -// { -// //Create Model -// Student student = new Student -// { -// Balance = 20000.5788 -// }; -// //Template -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"Original Balance: {{ Balance }}, #,##0 Balance: {{ Balance:#,##0 }}, N5 Balance: {{ Balance:N5 }}" -// }; -// string generatedTemplate = template.Map(student); -// string expectedString = "Original Balance: 20000.5788, #,##0 Balance: 20,001, N5 Balance: 20,000.57880"; -// Assert.Equal(expectedString, generatedTemplate, false, true, true); -// } - -// [Fact] -// public void Should_Accept_DateTime_To_String_Formatting() -// { -// //Lets see how it can handle multiple : {{ RegDate:yyyy-MM-dd HH:mm tt }} -// Student student = new Student -// { -// RegDate = new DateTime(2022, 11, 27, 18, 13, 59) -// }; -// //Template -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"Original RegDate: {{ RegDate }}, yyyy RegDate: {{ RegDate:yyyy }}, yyyy-MM-dd HH:mm tt RegDate: {{ RegDate:yyyy-MM-dd HH:mm tt }}" -// }; -// string generatedTemplate = template.Map(student); -// string expectedString = "Original RegDate: 11/27/2022 6:13:59 PM, yyyy RegDate: 2022, yyyy-MM-dd HH:mm tt RegDate: 2022-11-27 18:13 PM"; -// Assert.Equal(expectedString, generatedTemplate, false, true, true); -// } - - -// [Fact] -// public void Should_Accept_String_To_MD5_Formatting() -// { -// //Create Model -// Student student = new Student -// { -// StudentName = "John DOE" -// }; -// //Template -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"Original String: {{ StudentName }} | To MD5 String: {{ StudentName:ToMD5 }}" -// }; -// string generatedTemplate = template.Map(student); -// string expectedString = "Original String: John DOE | To MD5 String: 82AF64057A5F0D528CEE6F55D05823D7"; -// Assert.Equal(expectedString, generatedTemplate, false, true, true); -// } - - -// [Fact] -// public void Should_Accept_String_To_BASE64_Formatting() -// { -// //Create Model -// Student student = new Student -// { -// StudentName = "John DOE" -// }; -// //Template -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"Original String: {{ StudentName }} | To BASE64 String: {{ StudentName:ToBase64 }}" -// }; -// string generatedTemplate = template.Map(student); -// string expectedString = "Original String: John DOE | To BASE64 String: Sm9obiBET0U="; -// Assert.Equal(expectedString, generatedTemplate, false, true, true); -// } - -// [Fact] -// public void Should_Accept_String_From_BASE64_Formatting() -// { -// //Create Model -// Student student = new Student -// { -// StudentName = "Sm9obiBET0U=" -// }; -// //Template -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"Original String: {{ StudentName }} | From BASE64 String: {{ StudentName:FromBase64 }}" -// }; -// string generatedTemplate = template.Map(student); -// string expectedString = "Original String: Sm9obiBET0U= | From BASE64 String: John DOE"; -// Assert.Equal(expectedString, generatedTemplate, false, true, true); -// } - -// [Theory] -// [InlineData("Alice Johnson", "13")] -// [InlineData("Bob", "3")] -// [InlineData("", "0")] -// [InlineData(null, "")] -// public void Should_Return_Correct_Length_Of_StudentName(string studentName, string expectedLength) -// { -// // Create Model -// Student student = new Student -// { -// StudentName = studentName -// }; - -// // Template -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"{{ StudentName:length }}" -// }; - -// string generatedTemplate = template.Map(student); - -// Assert.Equal(expectedLength, generatedTemplate, ignoreCase: false, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); -// } -// } -//} From acb926c9d321ff17db4db141b5f7a77b31d85aa0 Mon Sep 17 00:00:00 2001 From: "George Njeri (Swagfin)" Date: Tue, 7 Oct 2025 22:34:19 +0300 Subject: [PATCH 05/12] chore: if condition block tests --- .../CognitiveMapTests.cs | 2 +- ObjectSemantics.NET.Tests/IfConditionTests.cs | 233 +++++++++--------- .../MoqModels/Invoice.cs | 13 - ObjectSemantics.NET.Tests/MoqModels/Person.cs | 5 +- .../MoqModels/Student.cs | 24 -- .../Engine/EngineAlgorithim.cs | 7 +- .../Engine/Models/ReplaceCode.cs | 10 - ObjectSemantics.NET/TemplateMapper.cs | 4 +- 8 files changed, 127 insertions(+), 171 deletions(-) delete mode 100644 ObjectSemantics.NET.Tests/MoqModels/Invoice.cs delete mode 100644 ObjectSemantics.NET.Tests/MoqModels/Student.cs diff --git a/ObjectSemantics.NET.Tests/CognitiveMapTests.cs b/ObjectSemantics.NET.Tests/CognitiveMapTests.cs index 5c9bf56..f909a0d 100644 --- a/ObjectSemantics.NET.Tests/CognitiveMapTests.cs +++ b/ObjectSemantics.NET.Tests/CognitiveMapTests.cs @@ -44,7 +44,7 @@ public void Additional_Headers_Should_Also_Be_Mapped() //additional params (outside the class) Dictionary additionalParams = new Dictionary { - { "Occupation","Developer"}, + { "Occupation", "Developer"}, { "DateOfBirth", new DateTime(1995, 01, 01) } }; diff --git a/ObjectSemantics.NET.Tests/IfConditionTests.cs b/ObjectSemantics.NET.Tests/IfConditionTests.cs index 489c20d..d250295 100644 --- a/ObjectSemantics.NET.Tests/IfConditionTests.cs +++ b/ObjectSemantics.NET.Tests/IfConditionTests.cs @@ -1,117 +1,116 @@ -//using ObjectSemantics.NET.Tests.MoqModels; -//using System.Collections.Generic; -//using Xunit; - -//namespace ObjectSemantics.NET.Tests -//{ -// public class IfConditionTests -// { -// [Theory] -// [InlineData(1, "Valid")] -// [InlineData(0, "Invalid")] -// public void Should_Render_If_Block_When_Condition_Is_True(int id, string expected) -// { -// var model = new Invoice { Id = id }; - -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"{{ #if(Id == 1) }}Valid{{ #else }}Invalid{{ #endif }}" -// }; - -// string result = template.Map(model); -// Assert.Equal(expected, result); -// } - - -// [Theory] -// [InlineData(18, "Minor")] -// [InlineData(21, "Adult")] -// [InlineData(5, "Minor")] -// public void Should_Handle_LessThan_Or_Equal(int age, string expected) -// { -// var model = new Student { Age = age }; - -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"{{ #if(Age <= 18) }}Minor{{ #else }}Adult{{ #endif }}" -// }; - -// var result = template.Map(model); -// Assert.Equal(expected, result); -// } - - -// [Theory] -// [InlineData(1, "1")] -// [InlineData(0, "Error")] -// [InlineData(-1, "Error")] -// [InlineData(5, "5")] -// [InlineData(+2, "2")] -// public void Should_Handle_Whitespace_And_Case_Insensitive_Condition(int id, string expected) -// { -// var model = new Invoice { Id = id }; - -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"{{ #if( id > 0 ) }}{{id}}{{ #else }}Error{{ #endif }}" -// }; - -// var result = template.Map(model); -// Assert.Equal(expected, result); -// } - - -// [Fact] -// public void Should_Render_If_Block_Without_Else_When_True() -// { -// var model = new Student { IsActive = true }; - -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"Student: John {{ #if(IsActive == true) }}[Is Active]{{ #endif }}" -// }; - -// var result = template.Map(model); -// Assert.Equal("Student: John [Is Active]", result); -// } - -// [Fact] -// public void Should_Evaluate_If_Enumerable_Count() -// { -// var model = new Student -// { -// Invoices = new List -// { -// new Invoice{ Id = 2, RefNo = "INV_002" }, -// new Invoice{ Id = 1, RefNo = "INV_001" } -// } -// }; - -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"{{ #if(Invoices == 2) }}Matched{{ #else }}Not Matched{{ #endif }}" -// }; - -// var result = template.Map(model); -// Assert.Equal("Matched", result); -// } - -// [Fact] -// public void Should_Evaluate_Empty_Enumerable_As_Zero() -// { -// var model = new Student -// { -// Invoices = new List() -// }; - -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"{{ #if(Invoices == 0) }}No invoices available{{ #else }}Invoices Found{{ #endif }}" -// }; - -// var result = template.Map(model); -// Assert.Equal("No invoices available", result); -// } - -// } -//} +using ObjectSemantics.NET.Tests.MoqModels; +using System.Collections.Generic; +using Xunit; + +namespace ObjectSemantics.NET.Tests +{ + public class IfConditionTests + { + [Theory] + [InlineData(40, "Adult")] + [InlineData(18, "Adult")] + [InlineData(0, "Minor")] + [InlineData(-7, "Minor")] + public void Should_Render_If_GreaterOrEqual_Condition_Block(int age, string expected) + { + Person person = new Person + { + Age = age + }; + + string template = @"{{ #if(Age >= 18) }}Adult{{ #else }}Minor{{ #endif }}"; + + string result = person.Map(template); + Assert.Equal(expected, result); + } + + + [Theory] + [InlineData(40, "Adult 40")] + [InlineData(18, "Adult 18")] + [InlineData(0, "Minor 0")] + [InlineData(-7, "Minor -7")] + public void Should_Resolve_Variables_Inside_If_Condition(int age, string expected) + { + Person person = new Person + { + Age = age + }; + + string template = @"{{ #if( Age > 17 ) }}Adult {{Age}}{{ #else }}Minor {{Age}}{{ #endif }}"; + + var result = person.Map(template); + Assert.Equal(expected, result); + } + + + [Theory] + [InlineData(40, "")] + [InlineData(18, "")] + [InlineData(0, "NoDrinkingBeer")] + [InlineData(-7, "NoDrinkingBeer")] + public void Should_Render_If_Block_Without_Else_When_True(int age, string expected) + { + Person person = new Person + { + Age = age + }; + + string template = @"{{ #if(Age < 18) }}NoDrinkingBeer{{ #endif }}"; + + string result = person.Map(template); + Assert.Equal(expected, result); + } + + [Fact] + public void Should_Evaluate_Enumerable_Count_Inside_If_Block() + { + + Person person = new Person + { + MyCars = new List + { + new Car { Make = "BMW" }, + new Car { Make = "Rolls-Royce" } + } + }; + + string template = @"{{ #if(MyCars == 2) }}Has 2 Cars{{ #endif }}"; + + string result = person.Map(template); + Assert.Equal("Has 2 Cars", result); + } + + [Fact] + public void Should_Evaluate_Empty_Enumerable_As_Zero() + { + Person person = new Person + { + MyCars = null + }; + + string template = @"{{ #if(MyCars == 0) }}Zero Cars{{ #endif }}"; + + string result = person.Map(template); + Assert.Equal("Zero Cars", result); + } + + [Theory] + [InlineData("John DoeX2", "Yes")] + [InlineData("John Doe", "No")] + [InlineData("Doe", "No")] + [InlineData("John ", "No")] + public void Should_Evaluate_String_Conditions_If_Blocks(string name, string expected) + { + Person person = new Person + { + Name = name + }; + + string template = @"{{ #if(Name == John DoeX2) }}Yes{{ #else }}No{{ #endif }}"; + + string result = person.Map(template); + Assert.Equal(expected, result); + } + } +} diff --git a/ObjectSemantics.NET.Tests/MoqModels/Invoice.cs b/ObjectSemantics.NET.Tests/MoqModels/Invoice.cs deleted file mode 100644 index 7b43786..0000000 --- a/ObjectSemantics.NET.Tests/MoqModels/Invoice.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace ObjectSemantics.NET.Tests.MoqModels -{ - internal class Invoice - { - public int Id { get; set; } - public string RefNo { get; set; } - public string Narration { get; set; } - public double Amount { get; set; } - public DateTime InvoiceDate { get; set; } - } -} diff --git a/ObjectSemantics.NET.Tests/MoqModels/Person.cs b/ObjectSemantics.NET.Tests/MoqModels/Person.cs index 76bfa43..fca6269 100644 --- a/ObjectSemantics.NET.Tests/MoqModels/Person.cs +++ b/ObjectSemantics.NET.Tests/MoqModels/Person.cs @@ -1,8 +1,11 @@ -namespace ObjectSemantics.NET.Tests.MoqModels +using System.Collections.Generic; + +namespace ObjectSemantics.NET.Tests.MoqModels { public class Person { public string Name { get; set; } public int Age { get; set; } + public List MyCars { get; set; } = new List(); } } diff --git a/ObjectSemantics.NET.Tests/MoqModels/Student.cs b/ObjectSemantics.NET.Tests/MoqModels/Student.cs deleted file mode 100644 index 929e3de..0000000 --- a/ObjectSemantics.NET.Tests/MoqModels/Student.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace ObjectSemantics.NET.Tests.MoqModels -{ - internal class Student - { - public Guid Id { get; set; } = Guid.NewGuid(); - public string StudentName { get; set; } - public double Balance { get; set; } - public int Age { get; set; } - public DateTime RegDate { get; set; } = DateTime.Now; - public List Invoices { get; set; } = new List(); - public string[] ArrayOfString { get; set; } = new string[] { }; - public double[] ArrayOfDouble { get; set; } = new double[] { }; - public bool IsActive { get; set; } - public List StudentClockInDetails { get; set; } = new List(); - } - class StudentClockInDetail - { - public DateTime? LastClockedInDate { get; set; } = null; - public long? LastClockedInPoints { get; set; } = null; - } -} diff --git a/ObjectSemantics.NET/Engine/EngineAlgorithim.cs b/ObjectSemantics.NET/Engine/EngineAlgorithim.cs index 1db10ee..a7ef029 100644 --- a/ObjectSemantics.NET/Engine/EngineAlgorithim.cs +++ b/ObjectSemantics.NET/Engine/EngineAlgorithim.cs @@ -78,7 +78,8 @@ internal static class EngineAlgorithim foreach (ReplaceCode objLoopCode in objLoop.ReplaceObjCodes) { - if (objLoopCode.TargetPropertyName == ".") + string propName = objLoopCode.GetTargetPropertyName(); + if (propName == ".") { ExtractedObjProperty tempProp = new ExtractedObjProperty { @@ -90,7 +91,7 @@ internal static class EngineAlgorithim } else { - if (rowMap.TryGetValue(objLoopCode.TargetPropertyName, out ExtractedObjProperty p)) + if (rowMap.TryGetValue(propName, out ExtractedObjProperty p)) activeRow.Replace(objLoopCode.ReplaceRef, p.GetPropertyDisplayString(objLoopCode.GetFormattingCommand(), options)); else activeRow.Replace(objLoopCode.ReplaceRef, objLoopCode.ReplaceCommand); @@ -106,7 +107,7 @@ internal static class EngineAlgorithim // ---- Direct Replacements ---- foreach (ReplaceCode replaceCode in template.ReplaceCodes) { - if (propMap.TryGetValue(replaceCode.TargetPropertyName, out ExtractedObjProperty property)) + if (propMap.TryGetValue(replaceCode.GetTargetPropertyName(), out ExtractedObjProperty property)) result.Replace(replaceCode.ReplaceRef, property.GetPropertyDisplayString(replaceCode.GetFormattingCommand(), options)); else diff --git a/ObjectSemantics.NET/Engine/Models/ReplaceCode.cs b/ObjectSemantics.NET/Engine/Models/ReplaceCode.cs index 947ba1b..7ba59df 100644 --- a/ObjectSemantics.NET/Engine/Models/ReplaceCode.cs +++ b/ObjectSemantics.NET/Engine/Models/ReplaceCode.cs @@ -4,15 +4,5 @@ internal class ReplaceCode { public string ReplaceRef { get; set; } public string ReplaceCommand { get; set; } - public string TargetPropertyName - { - get - { - if (string.IsNullOrEmpty(ReplaceCommand)) - return string.Empty; - int colonIndex = ReplaceCommand.IndexOf(':'); - return colonIndex > 0 ? ReplaceCommand.Substring(0, colonIndex).Trim() : ReplaceCommand.Trim(); - } - } } } \ No newline at end of file diff --git a/ObjectSemantics.NET/TemplateMapper.cs b/ObjectSemantics.NET/TemplateMapper.cs index 80b7b40..947c636 100644 --- a/ObjectSemantics.NET/TemplateMapper.cs +++ b/ObjectSemantics.NET/TemplateMapper.cs @@ -16,7 +16,7 @@ public static class TemplateMapper /// /// /// - public static string Map(this string template, T record, Dictionary additionalKeyValues = null, TemplateMapperOptions options = null) where T : new() + public static string Map(this string template, T record, Dictionary additionalKeyValues = null, TemplateMapperOptions options = null) where T : class, new() { return Map(record, template, additionalKeyValues, options); } @@ -31,7 +31,7 @@ public static class TemplateMapper /// /// /// - public static string Map(this T record, string template, Dictionary additionalKeyValues = null, TemplateMapperOptions options = null) where T : new() + public static string Map(this T record, string template, Dictionary additionalKeyValues = null, TemplateMapperOptions options = null) where T : class, new() { if (record == null) return string.Empty; if (template == null) throw new Exception("Template Object can't be NULL"); From b91828502a9ce70cbe0cf9015e1f4bf3d4d41790 Mon Sep 17 00:00:00 2001 From: "George Njeri (Swagfin)" Date: Wed, 8 Oct 2025 00:21:55 +0300 Subject: [PATCH 06/12] chore: tiny patch --- .../EnumerableLoopTests.cs | 365 +++++++----------- ObjectSemantics.NET.Tests/MoqModels/Person.cs | 2 + .../Engine/EngineAlgorithim.cs | 8 +- .../Engine/Extensions/StringExtensions.cs | 47 +++ 4 files changed, 190 insertions(+), 232 deletions(-) diff --git a/ObjectSemantics.NET.Tests/EnumerableLoopTests.cs b/ObjectSemantics.NET.Tests/EnumerableLoopTests.cs index 209e83b..7609517 100644 --- a/ObjectSemantics.NET.Tests/EnumerableLoopTests.cs +++ b/ObjectSemantics.NET.Tests/EnumerableLoopTests.cs @@ -1,227 +1,138 @@ -//using ObjectSemantics.NET.Tests.MoqModels; -//using System; -//using System.Collections.Generic; -//using Xunit; - -//namespace ObjectSemantics.NET.Tests -//{ -// public class EnumerableLoopTests -// { - -// [Fact] -// public void Should_Map_Enumerable_Collection_In_Object() -// { -// //Create Model -// Student student = new Student -// { -// StudentName = "John Doe", -// Invoices = new List -// { -// new Invoice{ Id=2, RefNo="INV_002",Narration="Grade II Fees Invoice", Amount=2000, InvoiceDate= new DateTime(2023, 04, 01) }, -// new Invoice{ Id=1, RefNo="INV_001",Narration="Grade I Fees Invoice", Amount=320, InvoiceDate= new DateTime(2022, 08, 01) } -// } -// }; -// //Template -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"{{ StudentName }} Invoices -//{{ #foreach(invoices) }} -// -// {{ Id }} -// {{ RefNo }} -// {{ Narration }} -// {{ Amount:N0 }} -// {{ InvoiceDate:yyyy-MM-dd }} -// -//{{ #endforeach }}" -// }; -// string generatedTemplate = template.Map(student); -// string expectedResult = @"John Doe Invoices - -// -// 2 -// INV_002 -// Grade II Fees Invoice -// 2,000 -// 2023-04-01 -// - -// -// 1 -// INV_001 -// Grade I Fees Invoice -// 320 -// 2022-08-01 -// -//"; -// Assert.Equal(expectedResult, generatedTemplate, false, true, true); -// } - - -// [Fact] -// public void Should_Map_Enumerable_Collection_SingleLine_Test() -// { -// //Create Model -// Student student = new Student -// { -// StudentName = "John Doe", -// Invoices = new List -// { -// new Invoice{ Id=2, RefNo="INV_002",Narration="Grade II Fees Invoice", Amount=2000, InvoiceDate= new DateTime(2023, 04, 01) }, -// new Invoice{ Id=1, RefNo="INV_001",Narration="Grade I Fees Invoice", Amount=320, InvoiceDate= new DateTime(2022, 08, 01) } -// } -// }; -// //Template -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"{{ #foreach(invoices) }} [{{RefNo}}] {{ #endforeach }}" -// }; -// string generatedTemplate = template.Map(student); -// string expectedResult = " [INV_002] [INV_001] "; -// Assert.Equal(expectedResult, generatedTemplate, false, true, true); -// } - - -// [Fact] -// public void Should_Map_Multiple_Same_Property_Enumerable_Collection_On_Same_Template() -// { -// //Create Model -// Student student = new Student -// { -// StudentName = "John Doe", -// Invoices = new List -// { -// new Invoice{ Id=2, RefNo="INV_002",Narration="Grade II Fees Invoice", Amount=2000, InvoiceDate= new DateTime(2023, 04, 01) }, -// new Invoice{ Id=1, RefNo="INV_001",Narration="Grade I Fees Invoice", Amount=320, InvoiceDate= new DateTime(2022, 08, 01) } -// } -// }; -// //Template -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @" -//{{ StudentName }} Invoices -//LOOP #1 -//{{ #foreach(invoices) }} -//
{{ Id }} On Loop #1
-//{{ #endforeach }} -//LOOP #2 -//{{ #foreach(invoices) }} -//
{{ Id }} On Loop #2
-//{{ #endforeach }}" -// }; -// string generatedTemplate = template.Map(student); -// string expectedResult = @" -//John Doe Invoices -//LOOP #1 - -//
2 On Loop #1
- -//
1 On Loop #1
- -//LOOP #2 - -//
2 On Loop #2
- -//
1 On Loop #2
-//"; -// Assert.Equal(expectedResult, generatedTemplate, false, true, true); -// } - - -// [Fact] -// public void Should_Map_Multiple_Different_Property_Enumerable_Collection_On_Same_Template() -// { -// //Create Model -// Student student = new Student -// { -// StudentName = "John Doe", -// Invoices = new List -// { -// new Invoice{ Id=2, RefNo="INV_002",Narration="Grade II Fees Invoice", Amount=2000, InvoiceDate= new DateTime(2023, 04, 01) }, -// new Invoice{ Id=1, RefNo="INV_001",Narration="Grade I Fees Invoice", Amount=320, InvoiceDate= new DateTime(2022, 08, 01) } -// }, -// StudentClockInDetails = new List -// { -// new StudentClockInDetail{ LastClockedInDate = new DateTime(2024, 04, 01), LastClockedInPoints = 10 }, -// new StudentClockInDetail{ LastClockedInDate = new DateTime(2024, 04, 02), LastClockedInPoints = 30 } -// } -// }; -// //Template -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @" -//{{ StudentName }} Invoices -//LOOP #1 -//{{ #foreach(invoices) }} -//
{{ Id }} On Loop #1
-//{{ #endforeach }} -//LOOP #2 -//{{ #foreach(studentClockInDetails) }} -//
Got {{ LastClockedInPoints }} for {{ LastClockedInDate:yyyy-MM-dd }}
-//{{ #endforeach }}" -// }; -// string generatedTemplate = template.Map(student); -// string expectedResult = @" -//John Doe Invoices -//LOOP #1 - -//
2 On Loop #1
- -//
1 On Loop #1
- -//LOOP #2 - -//
Got 10 for 2024-04-01
- -//
Got 30 for 2024-04-02
-//"; - -// Assert.Equal(expectedResult, generatedTemplate, false, true, true); -// } - -// [Fact] -// public void Should_Map_Array_Of_String_With_Formatting() -// { -// //Create Model -// Student student = new Student -// { -// ArrayOfString = new string[] -// { -// "String 001", -// "String 002" -// } -// }; -// //Template -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"{{ #foreach(ArrayOfString) }} {{ . }} | {{ .:uppercase }} {{ #endforeach }}" -// }; -// string generatedTemplate = template.Map(student); -// string expectedResult = " String 001 | STRING 001 String 002 | STRING 002 "; -// Assert.Equal(expectedResult, generatedTemplate, false, true, true); -// } - - -// [Fact] -// public void Should_Map_Array_Of_Double_With_Formatting_Test() -// { -// //Create Model -// Student student = new Student -// { -// ArrayOfDouble = new double[] -// { -// 1000.15, -// 2000.22 -// } -// }; -// //Template -// var template = new ObjectSemanticsTemplate -// { -// FileContents = @"{{ #foreach(ArrayOfDouble) }} {{ . }} | {{ .:N0 }} {{ #endforeach }}" -// }; -// string generatedTemplate = template.Map(student); -// string expectedResult = " 1000.15 | 1,000 2000.22 | 2,000 "; -// Assert.Equal(expectedResult, generatedTemplate, false, true, true); -// } -// } -//} +using ObjectSemantics.NET.Tests.MoqModels; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace ObjectSemantics.NET.Tests +{ + public class EnumerableLoopTests + { + + [Fact] + public void Should_Map_Enumerable_Collection_SingleLine() + { + //Create Model + Person person = new Person + { + MyCars = new List + { + new Car { Make = "BMW", Year = 2023 }, + new Car { Make = "Rolls-Royce", Year = 2020 } + } + }; + //Template + string template = @"{{ #foreach(MyCars) }}I own a {{ Year }} {{ Make }}.{{ #endforeach }}"; + + string result = person.Map(template); + + string expectedResult = $"I own a {person.MyCars[0].Year} {person.MyCars[0].Make}.I own a {person.MyCars[1].Year} {person.MyCars[1].Make}."; + Assert.Equal(expectedResult, result); + } + + [Fact] + public void Should_Map_Enumerable_Collection_MultiLine() + { + //Create Model + Person person = new Person + { + Name = "John Doe", + MyCars = new List + { + new Car { Make = "BMW", Year = 2023 }, + new Car { Make = "Rolls-Royce", Year = 2020 } + } + }; + //Template + string template = @" +{{ Name }}'s Cars +{{ #foreach(MyCars) }} + - {{ Year }} {{ Make }} +{{ #endforeach }}"; + + string result = person.Map(template); + + string expectedResult = @$" +{person.Name}'s Cars + - {person.MyCars[0].Year} {person.MyCars[0].Make} + - {person.MyCars[1].Year} {person.MyCars[1].Make} +"; + Assert.Equal(expectedResult, result); + } + + + + [Fact] + public void Should_Map_Multiple_Enumerable_Collections() + { + //Create Model + Person person = new Person + { + MyCars = new List + { + new Car { Make = "Honda" }, + new Car { Make = "Toyota" } + }, + MyDreamCars = new List + { + new Car { Make = "BWM" }, + new Car { Make = "Rolls-Royce" } + } + }; + //Template + string template = @" +My Cars +{{ #foreach(MyCars) }} + - {{ Make }} +{{ #endforeach }} +My Dream Cars +{{ #foreach(MyDreamCars) }} + * {{ Make }} +{{ #endforeach }} +"; + + string result = person.Map(template); + + string expectedResult = @$" +My Cars + - {person.MyCars[0].Make} + - {person.MyCars[1].Make} +My Dream Cars + - {person.MyDreamCars[0].Make} + - {person.MyDreamCars[1].Make} +"; + Assert.Equal(expectedResult, result); + } + + [Fact] + public void Should_Map_Array_Of_String() + { + //Create Model + Person person = new Person + { + MyFriends = new string[] { "Morgan", "George", "Jane" } + }; + //Template + string template = @"{{ #foreach(MyFriends) }} {{ . }} {{ #endforeach }}"; + + string generatedTemplate = person.Map(template); + string expectedResult = string.Join(string.Empty, person.MyFriends); + Assert.Equal(expectedResult, generatedTemplate, false, true, true); + } + + [Fact] + public void Should_Map_Array_Of_String_With_Formatting() + { + //Create Model + Person person = new Person + { + MyFriends = new string[] { "Morgan", "George", "Jane" } + }; + //Template + string template = @"{{ #foreach(MyFriends) }} {{ .:uppercase }} {{ #endforeach }}"; + + string generatedTemplate = person.Map(template); + string expectedResult = string.Join(string.Empty, person.MyFriends.Select(x => x.ToUpper())); + Assert.Equal(expectedResult, generatedTemplate, false, true, true); + } + } +} diff --git a/ObjectSemantics.NET.Tests/MoqModels/Person.cs b/ObjectSemantics.NET.Tests/MoqModels/Person.cs index fca6269..98235a0 100644 --- a/ObjectSemantics.NET.Tests/MoqModels/Person.cs +++ b/ObjectSemantics.NET.Tests/MoqModels/Person.cs @@ -7,5 +7,7 @@ public class Person public string Name { get; set; } public int Age { get; set; } public List MyCars { get; set; } = new List(); + public List MyDreamCars { get; set; } = new List(); + public string[] MyFriends { get; set; } } } diff --git a/ObjectSemantics.NET/Engine/EngineAlgorithim.cs b/ObjectSemantics.NET/Engine/EngineAlgorithim.cs index a7ef029..3d17e47 100644 --- a/ObjectSemantics.NET/Engine/EngineAlgorithim.cs +++ b/ObjectSemantics.NET/Engine/EngineAlgorithim.cs @@ -108,12 +108,10 @@ internal static class EngineAlgorithim foreach (ReplaceCode replaceCode in template.ReplaceCodes) { if (propMap.TryGetValue(replaceCode.GetTargetPropertyName(), out ExtractedObjProperty property)) - result.Replace(replaceCode.ReplaceRef, - property.GetPropertyDisplayString(replaceCode.GetFormattingCommand(), options)); + result.Replace(replaceCode.ReplaceRef, property.GetPropertyDisplayString(replaceCode.GetFormattingCommand(), options)); else result.Replace(replaceCode.ReplaceRef, "{{ " + replaceCode.ReplaceCommand + " }}"); } - return result.ToString(); } @@ -147,10 +145,10 @@ internal static EngineRunnerTemplate GenerateRunnerTemplate(string fileContent) ReplaceObjLoopCode objLoop = new ReplaceObjLoopCode { ReplaceRef = refKey, - TargetObjectName = m.Groups[1].Value + TargetObjectName = m.Groups[1].Value?.Trim() ?? string.Empty }; - string loopBlock = m.Groups[2].Value; + string loopBlock = m.Groups[2].Value.Trim('\r', '\n'); loopBlock = DirectParamRegex.Replace(loopBlock, pm => { key++; diff --git a/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs b/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs index a0d49eb..f8ec7cd 100644 --- a/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs +++ b/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs @@ -25,6 +25,53 @@ public static string RemoveLastInstanceOfString(this string value, params char[] return value; } + public static string TrimCustom(this string data, char[] chars, int? takeCount) + { + if (string.IsNullOrEmpty(data) || chars == null || chars.Length == 0) + return data; + + // If takeCount is null or < 1, behave like normal Trim + if (takeCount == null || takeCount < 1) + return data.Trim(chars); + + int leftIndex = 0; + int rightIndex = data.Length - 1; + int leftCount = 0; + int rightCount = 0; + + // ⚡ Use simple lookup optimization + // Create a small fixed lookup array for faster char matching + bool[] lookup = new bool[char.MaxValue + 1]; + for (int i = 0; i < chars.Length; i++) + lookup[chars[i]] = true; + + // Trim from start + while (leftIndex < data.Length && leftCount < takeCount.Value) + { + char c = data[leftIndex]; + if (c < lookup.Length && lookup[c]) + { + leftIndex++; + leftCount++; + } + else break; + } + + // Trim from end + while (rightIndex >= leftIndex && rightCount < takeCount.Value) + { + char c = data[rightIndex]; + if (c < lookup.Length && lookup[c]) + { + rightIndex--; + rightCount++; + } + else break; + } + + return data.Substring(leftIndex, rightIndex - leftIndex + 1); + } + public static string ToMD5String(this string input) { try From f7980af7ab4cfc573082cfe136c037d1f73305b7 Mon Sep 17 00:00:00 2001 From: "George Njeri (Swagfin)" Date: Wed, 8 Oct 2025 00:36:23 +0300 Subject: [PATCH 07/12] chore: improved tests --- .../EnumerableLoopTests.cs | 34 +++++++++++-------- .../Engine/EngineAlgorithim.cs | 2 +- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/ObjectSemantics.NET.Tests/EnumerableLoopTests.cs b/ObjectSemantics.NET.Tests/EnumerableLoopTests.cs index 7609517..e7702e3 100644 --- a/ObjectSemantics.NET.Tests/EnumerableLoopTests.cs +++ b/ObjectSemantics.NET.Tests/EnumerableLoopTests.cs @@ -1,6 +1,5 @@ using ObjectSemantics.NET.Tests.MoqModels; using System.Collections.Generic; -using System.Linq; using Xunit; namespace ObjectSemantics.NET.Tests @@ -25,7 +24,7 @@ public void Should_Map_Enumerable_Collection_SingleLine() string result = person.Map(template); - string expectedResult = $"I own a {person.MyCars[0].Year} {person.MyCars[0].Make}.I own a {person.MyCars[1].Year} {person.MyCars[1].Make}."; + string expectedResult = $"I own a 2023 BMW.I own a 2020 Rolls-Royce."; Assert.Equal(expectedResult, result); } @@ -51,16 +50,17 @@ public void Should_Map_Enumerable_Collection_MultiLine() string result = person.Map(template); - string expectedResult = @$" -{person.Name}'s Cars - - {person.MyCars[0].Year} {person.MyCars[0].Make} - - {person.MyCars[1].Year} {person.MyCars[1].Make} + string expectedResult = @" +John Doe's Cars + + - 2023 BMW + + - 2020 Rolls-Royce "; Assert.Equal(expectedResult, result); } - [Fact] public void Should_Map_Multiple_Enumerable_Collections() { @@ -92,13 +92,19 @@ My Dream Cars string result = person.Map(template); - string expectedResult = @$" + string expectedResult = @" My Cars - - {person.MyCars[0].Make} - - {person.MyCars[1].Make} + + - Honda + + - Toyota + My Dream Cars - - {person.MyDreamCars[0].Make} - - {person.MyDreamCars[1].Make} + + * BWM + + * Rolls-Royce + "; Assert.Equal(expectedResult, result); } @@ -115,7 +121,7 @@ public void Should_Map_Array_Of_String() string template = @"{{ #foreach(MyFriends) }} {{ . }} {{ #endforeach }}"; string generatedTemplate = person.Map(template); - string expectedResult = string.Join(string.Empty, person.MyFriends); + string expectedResult = " Morgan George Jane "; Assert.Equal(expectedResult, generatedTemplate, false, true, true); } @@ -131,7 +137,7 @@ public void Should_Map_Array_Of_String_With_Formatting() string template = @"{{ #foreach(MyFriends) }} {{ .:uppercase }} {{ #endforeach }}"; string generatedTemplate = person.Map(template); - string expectedResult = string.Join(string.Empty, person.MyFriends.Select(x => x.ToUpper())); + string expectedResult = " MORGAN GEORGE JANE "; Assert.Equal(expectedResult, generatedTemplate, false, true, true); } } diff --git a/ObjectSemantics.NET/Engine/EngineAlgorithim.cs b/ObjectSemantics.NET/Engine/EngineAlgorithim.cs index 3d17e47..0b75f4e 100644 --- a/ObjectSemantics.NET/Engine/EngineAlgorithim.cs +++ b/ObjectSemantics.NET/Engine/EngineAlgorithim.cs @@ -148,7 +148,7 @@ internal static EngineRunnerTemplate GenerateRunnerTemplate(string fileContent) TargetObjectName = m.Groups[1].Value?.Trim() ?? string.Empty }; - string loopBlock = m.Groups[2].Value.Trim('\r', '\n'); + string loopBlock = m.Groups[2].Value; loopBlock = DirectParamRegex.Replace(loopBlock, pm => { key++; From 80b72c8951b6dc60072c738c99d2910856b54bb3 Mon Sep 17 00:00:00 2001 From: "George Njeri (Swagfin)" Date: Wed, 8 Oct 2025 00:50:59 +0300 Subject: [PATCH 08/12] chore: complexity unit tests --- ObjectSemantics.NET.Tests/ComplexityTests.cs | 63 ++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 ObjectSemantics.NET.Tests/ComplexityTests.cs diff --git a/ObjectSemantics.NET.Tests/ComplexityTests.cs b/ObjectSemantics.NET.Tests/ComplexityTests.cs new file mode 100644 index 0000000..1c3178d --- /dev/null +++ b/ObjectSemantics.NET.Tests/ComplexityTests.cs @@ -0,0 +1,63 @@ +using ObjectSemantics.NET.Tests.MoqModels; +using System.Collections.Generic; +using Xunit; + +namespace ObjectSemantics.NET.Tests +{ + public class ComplexityTests + { + + [Fact] + public void Should_Handle_ForEach_Loop_Inside_If_Block_Inline() + { + Person person = new Person + { + MyCars = new List + { + new Car { Make = "BMW" }, + new Car { Make = "Rolls-Royce" } + } + }; + + string template = @"{{ #if(MyCars > 0) }}{{ #foreach(MyCars) }}[{{ Make }}]{{ #endforeach }}{{ #endif }}"; + + string result = person.Map(template); + + string expected = "[BMW][Rolls-Royce]"; + Assert.Equal(expected, result); + } + + + [Fact] + public void Should_Handle_ForEach_Loop_Inside_If_Block_Multiline() + { + Person person = new Person + { + MyCars = new List + { + new Car { Make = "Toyota" }, + new Car { Make = "BMW" } + } + }; + + string template = @" +{{ #if(MyCars > 0) }} + {{ #foreach(MyCars) }} + - {{ Make:uppercase }} + {{ #endforeach }} +{{ #endif }}"; + + string result = person.Map(template); + + string expected = @" + + + - TOYOTA + + - BMW + +"; + Assert.Equal(expected, result); + } + } +} From 4f561a55be5f17f8d08439cc9c1c791c0fa064b5 Mon Sep 17 00:00:00 2001 From: "George Njeri (Swagfin)" Date: Wed, 8 Oct 2025 00:58:52 +0300 Subject: [PATCH 09/12] chore: Readme update --- README.md | 104 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 57 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 9e47216..47056ca 100644 --- a/README.md +++ b/README.md @@ -34,90 +34,100 @@ Install-Package ObjectSemantics.NET ## 🚀 Quick Start -### Example 1: Basic Object Property Mapping +### Example 1: Mapping Object Properties ```csharp -// Create model -Student student = new Student +Person person = new Person { - StudentName = "George Waynne", - Balance = 2510 + Name = "John Doe" }; -// Define template -var template = new ObjectSemanticsTemplate +// Define template and map it using the object +string result = person.Map("I am {{ Name }}!"); + +Console.WriteLine(result); +``` + +**Output:** +``` +I am John Doe! +``` + +--- + +### Example 2: Mapping Using String Extension + +```csharp +Person person = new Person { - FileContents = @"My Name is: {{ StudentName }} and my balance is {{ Balance:N2 }}" + Name = "Jane Doe" }; -// Map object to template -string result = template.Map(student); +// You can also start with the string template +string result = "I am {{ Name }}!".Map(person); Console.WriteLine(result); ``` **Output:** ``` -My Name is: George Waynne and my balance is 2,510.00 +I am Jane Doe! ``` --- -### Example 2: Mapping Enumerable Collections +### Example 3: Mapping Enumerable Collections (Looping) ```csharp -Student student = new Student +Person person = new Person { - StudentName = "John Doe", - Invoices = new List + Name = "John Doe", + MyCars = new List { - new Invoice { Id = 2, RefNo = "INV_002", Narration = "Grade II Fees Invoice", Amount = 2000, InvoiceDate = new DateTime(2023, 04, 01) }, - new Invoice { Id = 1, RefNo = "INV_001", Narration = "Grade I Fees Invoice", Amount = 320, InvoiceDate = new DateTime(2022, 08, 01) } + new Car { Make = "BMW", Year = 2023 }, + new Car { Make = "Rolls-Royce", Year = 2020 } } }; -var template = new ObjectSemanticsTemplate +string template = @" +{{ Name }}'s Cars +{{ #foreach(MyCars) }} + - {{ Year }} {{ Make }} +{{ #endforeach }}"; + +string result = person.Map(template); + +Console.WriteLine(result); +``` + +**Output:** +``` +John Doe's Cars + - 2023 BMW + - 2020 Rolls-Royce +``` + +--- + +### Example 4: Number Formatting Support + +```csharp +Car car = new Car { - FileContents = @"{{ StudentName }} Invoices -{{ #foreach(Invoices) }} - - {{ Id }} - {{ RefNo }} - {{ Narration }} - {{ Amount:N0 }} - {{ InvoiceDate:yyyy-MM-dd }} - -{{ #endforeach }}" + Price = 50000m }; -string result = template.Map(student); +string result = car.Map("{{ Price:#,##0 }} | {{ Price:N5 }}"); Console.WriteLine(result); ``` **Output:** ``` -John Doe Invoices - - - 2 - INV_002 - Grade II Fees Invoice - 2,000 - 2023-04-01 - - - - 1 - INV_001 - Grade I Fees Invoice - 320 - 2022-08-01 - +50,000 | 50,000.00000 ``` --- - ## 🧪 More Samples Explore more usage examples and edge cases in the test project: From 793a4e0986327d0a10ecab953d6af1683c865b50 Mon Sep 17 00:00:00 2001 From: "George Njeri (Swagfin)" Date: Wed, 8 Oct 2025 01:04:58 +0300 Subject: [PATCH 10/12] chore: updated readme --- README.md | 106 +++++++++++++++++++++++++----------------------------- 1 file changed, 49 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index cedd011..8f20d4f 100644 --- a/README.md +++ b/README.md @@ -27,103 +27,95 @@ Install-Package ObjectSemantics.NET ## 🚀 Quick Start -### Example 1: Basic Object Property Mapping +### Example 1: Mapping Object Properties ```csharp -// Create model -Student student = new Student +Person person = new Person { - StudentName = "George Waynne", - Balance = 2510 + Name = "John Doe" }; -// Define template -var template = new ObjectSemanticsTemplate +// Define template and map it using the object +string result = person.Map("I am {{ Name }}!"); + +Console.WriteLine(result); +``` + +**Output:** +``` +I am John Doe! +``` +--- + +### Example 2: Mapping Using String Extension + +```csharp +Person person = new Person { - FileContents = @"My Name is: {{ StudentName }} and my balance is {{ Balance:N2 }}" + Name = "Jane Doe" }; -// Map object to template -string result = template.Map(student); +// You can also start with the string template +string result = "I am {{ Name }}!".Map(person); Console.WriteLine(result); ``` **Output:** ``` -My Name is: George Waynne and my balance is 2,510.00 +I am Jane Doe! ``` - --- -### Example 2: Mapping Enumerable Collections +### Example 3: Mapping Enumerable Collections (Looping) ```csharp -Student student = new Student +Person person = new Person { - StudentName = "John Doe", - Invoices = new List + Name = "John Doe", + MyCars = new List { - new Invoice { Id = 2, RefNo = "INV_002", Narration = "Grade II Fees", Amount = 2000, InvoiceDate = new DateTime(2023, 04, 01) }, - new Invoice { Id = 1, RefNo = "INV_001", Narration = "Grade I Fees", Amount = 320, InvoiceDate = new DateTime(2022, 08, 01) } + new Car { Make = "BMW", Year = 2023 }, + new Car { Make = "Rolls-Royce", Year = 2020 } } }; -var template = new ObjectSemanticsTemplate -{ - FileContents = @"{{ StudentName }} Invoices -{{ #foreach(Invoices) }} - - {{ Id }} - {{ RefNo }} - {{ Narration }} - {{ Amount:N0 }} - {{ InvoiceDate:yyyy-MM-dd }} - -{{ #endforeach }}" -}; +string template = @" +{{ Name }}'s Cars +{{ #foreach(MyCars) }} + - {{ Year }} {{ Make }} +{{ #endforeach }}"; -string result = template.Map(student); +string result = person.Map(template); Console.WriteLine(result); ``` **Output:** ``` -John Doe Invoices - - - 2 - INV_002 - Grade II Fees - 2,000 - 2023-04-01 - - - - 1 - INV_001 - Grade I Fees - 320 - 2022-08-01 - +John Doe's Cars + - 2023 BMW + - 2020 Rolls-Royce ``` - --- -### Example 3: String Formatters - -Use format like `uppercase`, `lowercase`, `titlecase`, `length`. +### Example 4: Number Formatting Support ```csharp -FileContents = "Uppercase: {{ StudentName:uppercase }}, Length: {{ StudentName:length }}" +Car car = new Car +{ + Price = 50000m +}; + +string result = car.Map("{{ Price:#,##0 }} | {{ Price:N5 }}"); + +Console.WriteLine(result); ``` -Outputs: +**Output:** ``` -Uppercase: ALICE, Length: 5 +50,000 | 50,000.00000 ``` - --- ## 💡 More Examples & Documentation From ddd6f7eb40e24b5f72363777a517c6bf040a166e Mon Sep 17 00:00:00 2001 From: "George Njeri (Swagfin)" Date: Wed, 8 Oct 2025 01:07:26 +0300 Subject: [PATCH 11/12] chore: rm unused extension --- .../Engine/Extensions/StringExtensions.cs | 47 ------------------- 1 file changed, 47 deletions(-) diff --git a/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs b/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs index f8ec7cd..a0d49eb 100644 --- a/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs +++ b/ObjectSemantics.NET/Engine/Extensions/StringExtensions.cs @@ -25,53 +25,6 @@ public static string RemoveLastInstanceOfString(this string value, params char[] return value; } - public static string TrimCustom(this string data, char[] chars, int? takeCount) - { - if (string.IsNullOrEmpty(data) || chars == null || chars.Length == 0) - return data; - - // If takeCount is null or < 1, behave like normal Trim - if (takeCount == null || takeCount < 1) - return data.Trim(chars); - - int leftIndex = 0; - int rightIndex = data.Length - 1; - int leftCount = 0; - int rightCount = 0; - - // ⚡ Use simple lookup optimization - // Create a small fixed lookup array for faster char matching - bool[] lookup = new bool[char.MaxValue + 1]; - for (int i = 0; i < chars.Length; i++) - lookup[chars[i]] = true; - - // Trim from start - while (leftIndex < data.Length && leftCount < takeCount.Value) - { - char c = data[leftIndex]; - if (c < lookup.Length && lookup[c]) - { - leftIndex++; - leftCount++; - } - else break; - } - - // Trim from end - while (rightIndex >= leftIndex && rightCount < takeCount.Value) - { - char c = data[rightIndex]; - if (c < lookup.Length && lookup[c]) - { - rightIndex--; - rightCount++; - } - else break; - } - - return data.Substring(leftIndex, rightIndex - leftIndex + 1); - } - public static string ToMD5String(this string input) { try From 20478fc7b3de3b94f72aa3ed7151070ba3a9894d Mon Sep 17 00:00:00 2001 From: "George Njeri (Swagfin)" Date: Wed, 8 Oct 2025 01:10:40 +0300 Subject: [PATCH 12/12] chore: Tiny change to readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8f20d4f..004c893 100644 --- a/README.md +++ b/README.md @@ -104,17 +104,17 @@ John Doe's Cars ```csharp Car car = new Car { - Price = 50000m + Price = 50000 }; -string result = car.Map("{{ Price:#,##0 }} | {{ Price:N5 }}"); +string result = car.Map("{{ Price:#,##0 }} | {{ Price:N2 }}"); Console.WriteLine(result); ``` **Output:** ``` -50,000 | 50,000.00000 +50,000 | 50,000.00 ``` ---