Skip to content

Commit

Permalink
Add TemplateContext case insensitivity (#449)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrey Kuznetsov committed Jul 11, 2022
1 parent b849c54 commit fbd06d8
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 41 deletions.
47 changes: 30 additions & 17 deletions src/Scriban.Tests/TestRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public void TestUlong()
var input = @"{{if value > 0; 1; else; 2; end;}}";

var template = Template.Parse(input);
var result = template.Render(new { value = (ulong) 1} );
var result = template.Render(new { value = (ulong)1 });
Assert.AreEqual("1", result);
}

Expand All @@ -139,7 +139,7 @@ public void TesterFilterEvaluation()
{
var result = Template.Parse("{{['', '200', '','400'] | array.filter @string.empty}}").Evaluate(new TemplateContext());
Assert.IsInstanceOf<ScriptRange>(result);
var array = (ScriptRange) result;
var array = (ScriptRange)result;
Assert.AreEqual(2, array.Count);
Assert.AreEqual("", array[0]);
Assert.AreEqual("", array[1]);
Expand Down Expand Up @@ -169,7 +169,7 @@ public void TestGetTypeName()
Assert.AreEqual("array", context.GetTypeName(new ScriptArray()));
Assert.AreEqual("array", context.GetTypeName(new ScriptArray<float>()));
Assert.AreEqual("object", context.GetTypeName(new ScriptObject()));
Assert.AreEqual("function", context.GetTypeName(DelegateCustomAction.Create(() => {})));
Assert.AreEqual("function", context.GetTypeName(DelegateCustomAction.Create(() => { })));
Assert.AreEqual("enum", context.GetTypeName(ScriptLang.Default));
}

Expand Down Expand Up @@ -378,7 +378,7 @@ public void TestFunctionCallWithNoReturn()
1 + g(2)
-}}
");
var tc = new TemplateContext() {ErrorForStatementFunctionAsExpression = true};
var tc = new TemplateContext() { ErrorForStatementFunctionAsExpression = true };
Assert.Throws<ScriptRuntimeException>(() => template.Render(tc));
}
{
Expand All @@ -388,7 +388,7 @@ public void TestFunctionCallWithNoReturn()
1 + g(2)
-}}
");
var tc = new TemplateContext() {ErrorForStatementFunctionAsExpression = true};
var tc = new TemplateContext() { ErrorForStatementFunctionAsExpression = true };
var result = template.Render(tc);
Assert.AreEqual("11", result);
}
Expand All @@ -399,7 +399,7 @@ public void TestFunctionCallWithNoReturn()
1 + g(2) + g(-1)
-}}
");
var tc = new TemplateContext() {ErrorForStatementFunctionAsExpression = true};
var tc = new TemplateContext() { ErrorForStatementFunctionAsExpression = true };
var result = template.Render(tc);
Assert.AreEqual("5", result);
}
Expand All @@ -414,9 +414,9 @@ public void TestExplicitFunctionCall()
g(x,y,z) = x + y * 2 + z * 10
1 + g(1,2,3) }} {{ g(5,6,7) * g(1,2,3) + 1
}}");
var tc = new TemplateContext() {ErrorForStatementFunctionAsExpression = true};
var tc = new TemplateContext() { ErrorForStatementFunctionAsExpression = true };
var result = template.Render(tc);
Assert.AreEqual($"{1 + g(1,2,3)} {g(5,6,7) * g(1,2,3) + 1}", result);
Assert.AreEqual($"{1 + g(1, 2, 3)} {g(5, 6, 7) * g(1, 2, 3) + 1}", result);
}

int g(int x, int y, int z) => x + y * 2 + z * 10;
Expand Down Expand Up @@ -471,7 +471,7 @@ public void TestFunctionWithTemplateContextAndObjectParams()
public void TestInvalidConvertToInt()
{
var template = Template.ParseLiquid("{{html>0}}");
var ex = Assert.Catch<ScriptRuntimeException>(() => template.Render(new {x = 0}));
var ex = Assert.Catch<ScriptRuntimeException>(() => template.Render(new { x = 0 }));
Assert.AreEqual("<input>(1,7) : error : Unable to convert type `object` to int", ex.Message);
}

Expand Down Expand Up @@ -832,7 +832,7 @@ public void TestScriptObjectImport()
Assert.True(obj.ContainsKey("static_yoyo"));
var function = (IScriptCustomFunction)obj["static_yoyo"];
var context = new TemplateContext();
var result = function.Invoke(context, new ScriptFunctionCall(), new ScriptArray() {"a"}, null);
var result = function.Invoke(context, new ScriptFunctionCall(), new ScriptArray() { "a" }, null);
Assert.AreEqual("yoyo2 a", result);
}

Expand Down Expand Up @@ -992,7 +992,7 @@ public void TestWithCharProperty()

var template = Template.Parse("{{ model.char }}");
var context = new TemplateContext();
var result = template.Render(new {model = test});
var result = template.Render(new { model = test });
Assert.AreEqual("a", result);
}

Expand Down Expand Up @@ -1156,8 +1156,8 @@ public void TestRelaxedDictionaryIndexerAccess()
[Test]
public void TestIndexerOnNET()
{
var myobject = new MyObject() { FieldA = "yo"};
var result = Template.Parse("{{obj['field_a']}}").Render(new ScriptObject() {{"obj", myobject}});
var myobject = new MyObject() { FieldA = "yo" };
var result = Template.Parse("{{obj['field_a']}}").Render(new ScriptObject() { { "obj", myobject } });
Assert.AreEqual("yo", result);
}

Expand All @@ -1170,7 +1170,7 @@ public void TestItemIndexerOnNET_String_Getter()
{
[key] = expected
};
var result = Template.Parse($"{{{{obj['{key}']}}}}").Render(new ScriptObject() {{"obj", myobject}});
var result = Template.Parse($"{{{{obj['{key}']}}}}").Render(new ScriptObject() { { "obj", myobject } });
Assert.AreEqual(expected, result);
}
[Test]
Expand All @@ -1182,7 +1182,7 @@ public void TestItemIndexerOnNET_String_Setter()
{
[key] = "Initial"
};
_ = Template.Parse($"{{{{obj['{key}'] = '{expected}'}}}}").Render(new ScriptObject() {{"obj", myobject}});
_ = Template.Parse($"{{{{obj['{key}'] = '{expected}'}}}}").Render(new ScriptObject() { { "obj", myobject } });
Assert.AreEqual(expected, myobject[key]);
}
[Test]
Expand All @@ -1194,7 +1194,7 @@ public void TestItemIndexerOnNET_Integer_Getter()
{
[key] = expected
};
var result = Template.Parse($"{{{{obj[{key}]}}}}").Render(new ScriptObject() {{"obj", myobject}});
var result = Template.Parse($"{{{{obj[{key}]}}}}").Render(new ScriptObject() { { "obj", myobject } });
Assert.AreEqual(expected, result);
}
[Test]
Expand All @@ -1206,7 +1206,7 @@ public void TestItemIndexerOnNET_Integer_Setter()
{
[key] = "Initial"
};
_ = Template.Parse($"{{{{obj[{key}] = '{expected}'}}}}").Render(new ScriptObject() {{"obj", myobject}});
_ = Template.Parse($"{{{{obj[{key}] = '{expected}'}}}}").Render(new ScriptObject() { { "obj", myobject } });
Assert.AreEqual(expected, myobject[key]);
}

Expand All @@ -1222,6 +1222,19 @@ public void TestCaseInsensitiveLookupOnScriptObject()
Assert.AreEqual("Hello world!", result);
}

[Test]
public void TestCaseInsensitiveLookupOnHierarchy()
{
var obj = new ScriptObject(StringComparer.OrdinalIgnoreCase);
obj.Import(new { UPPERCASED = new { lowercased = 42 } }, renamer: mi => mi.Name);

var context = new TemplateContext(StringComparer.OrdinalIgnoreCase);
context.PushGlobal(obj);

var result = Template.Parse("{{UPPERCASED.lowercased}}-{{uppercased.LOWERCASED}}-{{UPPERCASED.LOWERCASED}}-{{uppercased.lowercased}}").Render(context);
TextAssert.AreEqual("42-42-42-42", result);
}

private class MyObject : MyStaticObject
{
public string FieldA;
Expand Down
16 changes: 10 additions & 6 deletions src/Scriban/Runtime/Accessors/TypedObjectAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@ class TypedObjectAccessor : IObjectAccessor
private readonly Dictionary<string, MemberInfo> _members;
private PropertyInfo _indexer;

public TypedObjectAccessor(Type targetType, MemberFilterDelegate filter, MemberRenamerDelegate renamer)
public TypedObjectAccessor(Type targetType, MemberFilterDelegate filter, MemberRenamerDelegate renamer) : this(targetType, null, filter, renamer)
{
}

public TypedObjectAccessor(Type targetType, IEqualityComparer<string> keyComparer, MemberFilterDelegate filter, MemberRenamerDelegate renamer)
{
_type = targetType ?? throw new ArgumentNullException(nameof(targetType));
_filter = filter;
_renamer = renamer ?? StandardMemberRenamer.Default;
_members = new Dictionary<string, MemberInfo>();
_members = new Dictionary<string, MemberInfo>(keyComparer);
PrepareMembers();
}

Expand Down Expand Up @@ -65,7 +69,7 @@ public bool TryGetValue(TemplateContext context, SourceSpan span, object target,
return true;
}

var propertyAccessor = (PropertyInfo) memberAccessor;
var propertyAccessor = (PropertyInfo)memberAccessor;
value = propertyAccessor.GetValue(target);
return true;
}
Expand All @@ -79,7 +83,7 @@ public bool TryGetItem(TemplateContext context, SourceSpan span, object target,
value = default;
return false;
}
value = this._indexer.GetValue(target, new []{index});
value = this._indexer.GetValue(target, new[] { index });
return true;
}

Expand All @@ -89,7 +93,7 @@ public bool TrySetItem(TemplateContext context, SourceSpan span, object target,
{
return false;
}
_indexer.SetValue(target, value, new []{index});
_indexer.SetValue(target, value, new[] { index });
return true;
}

Expand All @@ -107,7 +111,7 @@ public bool TrySetValue(TemplateContext context, SourceSpan span, object target,
}

var propertyAccessor = (PropertyInfo)memberAccessor;
propertyAccessor.SetValue(target, context.ToObject(span, value, propertyAccessor.PropertyType));
propertyAccessor.SetValue(target, context.ToObject(span, value, propertyAccessor.PropertyType));

return true;
}
Expand Down
57 changes: 39 additions & 18 deletions src/Scriban/TemplateContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ partial class TemplateContext
private FastStack<Dictionary<object, object>> _availableTags;
private ScriptPipeArguments _currentPipeArguments;
private bool _previousTextWasNewLine;
private readonly IEqualityComparer<string> _keyComparer;

internal bool AllowPipeArguments => _getOrSetValueLevel <= 1;

Expand Down Expand Up @@ -104,15 +105,32 @@ partial class TemplateContext
/// <summary>
/// Initializes a new instance of the <see cref="T:Scriban.TemplateContext" /> class.
/// </summary>
public TemplateContext() : this(null)
public TemplateContext() : this(null, null)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="TemplateContext" /> class.
/// </summary>
/// <param name="builtin">The builtin object used to expose builtin functions, default is <see cref="GetDefaultBuiltinObject"/>.</param>
public TemplateContext(ScriptObject builtin)
public TemplateContext(ScriptObject builtin) : this(builtin, null)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="TemplateContext" /> class.
/// </summary>
/// <param name="keyComparer">Comparer to use when looking up members</param>
public TemplateContext(IEqualityComparer<string> keyComparer) : this(null, keyComparer)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="TemplateContext" /> class.
/// </summary>
/// <param name="builtin">The builtin object used to expose builtin functions, default is <see cref="GetDefaultBuiltinObject"/>.</param>
/// <param name="keyComparer">Comparer to use when looking up members</param>
public TemplateContext(ScriptObject builtin, IEqualityComparer<string> keyComparer)
{
BuiltinObject = builtin ?? GetDefaultBuiltinObject();
EnableOutput = true;
Expand All @@ -125,7 +143,7 @@ public TemplateContext(ScriptObject builtin)
LoopLimit = 1000;
RecursiveLimit = 100;
LimitToString = 0;
ObjectRecursionLimit=0;
ObjectRecursionLimit = 0;
MemberRenamer = StandardMemberRenamer.Default;

RegexTimeOut = TimeSpan.FromSeconds(10);
Expand Down Expand Up @@ -163,6 +181,9 @@ public TemplateContext(ScriptObject builtin)
_pipeArguments = new FastStack<ScriptPipeArguments>(4);
_availableScriptExpressionLists = new FastStack<List<ScriptExpression>>(4);
_availableReflectionArguments = new object[ScriptFunctionCall.MaximumParameterCount + 1][];

_keyComparer = keyComparer;

for (int i = 0; i < _availableReflectionArguments.Length; i++)
{
_availableReflectionArguments[i] = new object[i];
Expand Down Expand Up @@ -191,7 +212,7 @@ public TemplateContext(ScriptObject builtin)
/// Gets a boolean if the context is being used with liquid
/// </summary>
public bool IsLiquid { get; protected set; }

/// <summary>
/// If sets to <c>true</c>, the include statement will maintain the indent.
/// </summary>
Expand Down Expand Up @@ -347,7 +368,7 @@ public bool IndentWithInclude
/// Gets the number of <see cref="PushOutput()"/> that are pushed to this context.
/// </summary>
public int OutputCount => _outputs.Count;

/// <summary>
/// Gets the number of <see cref="PushCulture"/> that are pushed to this context.
/// </summary>
Expand Down Expand Up @@ -481,7 +502,7 @@ internal void ClearPipeArguments()

internal List<ScriptExpression> GetOrCreateListOfScriptExpressions(int capacity)
{
var list = _availableScriptExpressionLists.Count > 0 ? _availableScriptExpressionLists.Pop() : new List<ScriptExpression>();
var list = _availableScriptExpressionLists.Count > 0 ? _availableScriptExpressionLists.Pop() : new List<ScriptExpression>();
if (capacity > list.Capacity) list.Capacity = capacity;
return list;
}
Expand All @@ -499,13 +520,13 @@ internal object[] GetOrCreateReflectionArguments(int length)
// Don't try to allocate more than we can allocate
if (length >= _availableReflectionArguments.Length) return new object[length];

var reflectionArguments = _availableReflectionArguments[length] ?? new object[length];
if (length > 0)
{
_availableReflectionArguments[length] = (object[])reflectionArguments[0];
reflectionArguments[0] = null;
}
return reflectionArguments;
var reflectionArguments = _availableReflectionArguments[length] ?? new object[length];
if (length > 0)
{
_availableReflectionArguments[length] = (object[])reflectionArguments[0];
reflectionArguments[0] = null;
}
return reflectionArguments;
}

internal void ReleaseReflectionArguments(object[] reflectionArguments)
Expand Down Expand Up @@ -895,7 +916,7 @@ protected virtual IObjectAccessor GetMemberAccessorImpl(object target)
}
else
{
accessor = new TypedObjectAccessor(type, MemberFilter, MemberRenamer);
accessor = new TypedObjectAccessor(type, _keyComparer, MemberFilter, MemberRenamer);
}
return accessor;
}
Expand Down Expand Up @@ -1007,7 +1028,7 @@ internal enum LoopType
Queryable
}

internal bool StepLoop(ScriptLoopStatementBase loop, LoopType loopType = LoopType.Default )
internal bool StepLoop(ScriptLoopStatementBase loop, LoopType loopType = LoopType.Default)
{
Debug.Assert(_loops.Count > 0);

Expand Down Expand Up @@ -1099,7 +1120,7 @@ private object GetOrSetValue(ScriptExpression targetExpression, object valueToSe
throw new ScriptRuntimeException(targetExpression.Span, $"Unsupported target expression for assignment."); // unit test: 105-assign-error1.txt
}
}
catch (Exception readonlyException) when(_getOrSetValueLevel == 1 && !(readonlyException is ScriptRuntimeException))
catch (Exception readonlyException) when (_getOrSetValueLevel == 1 && !(readonlyException is ScriptRuntimeException))
{
throw new ScriptRuntimeException(targetExpression.Span, $"Unexpected exception while accessing target expression: {readonlyException.Message}", readonlyException);
}
Expand Down Expand Up @@ -1186,8 +1207,8 @@ public LiquidTemplateContext() : base(new LiquidBuiltinsFunctions())
EnableBreakAndContinueAsReturnOutsideLoop = true;
EnableRelaxedTargetAccess = true;

TemplateLoaderLexerOptions = new LexerOptions() {Lang = ScriptLang.Liquid};
TemplateLoaderParserOptions = new ParserOptions() {LiquidFunctionsToScriban = true};
TemplateLoaderLexerOptions = new LexerOptions() { Lang = ScriptLang.Liquid };
TemplateLoaderParserOptions = new ParserOptions() { LiquidFunctionsToScriban = true };
IsLiquid = true;
}
}
Expand Down

0 comments on commit fbd06d8

Please sign in to comment.