diff --git a/Liquid.NET.Tests/Constants/DictionaryValueTests.cs b/Liquid.NET.Tests/Constants/DictionaryValueTests.cs index 540ea2f..d30d151 100755 --- a/Liquid.NET.Tests/Constants/DictionaryValueTests.cs +++ b/Liquid.NET.Tests/Constants/DictionaryValueTests.cs @@ -58,5 +58,18 @@ public void It_Should_Dereference_A_Nested_Dictionary() Assert.That(result, Is.EqualTo("Result : Dict 3->Dict3")); } + [Test] + public void It_Should_Fail_When_Dereferencing_A_Missing_Property() + { + // Arrange + var dict = new Dictionary + { + }; + DictionaryValue dictValue = new DictionaryValue(dict); + + // Assert + Assert.That(dictValue.ValueAt("string1").HasValue, Is.False); + + } } } diff --git a/Liquid.NET.Tests/Constants/MissingValueTests.cs b/Liquid.NET.Tests/Constants/MissingValueTests.cs new file mode 100755 index 0000000..1de9719 --- /dev/null +++ b/Liquid.NET.Tests/Constants/MissingValueTests.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Liquid.NET.Constants; +using NUnit.Framework; + +namespace Liquid.NET.Tests.Constants +{ + [TestFixture] + public class MissingValueTests + { + [Test] + [TestCase("x", "x")] + [TestCase("d.x", "x")] + [TestCase("d[x]", "x")] + [TestCase("a.b.c", "a")] + public void It_Should_Display_An_Error_When_Dereferencing_Missing_Value(String varname, String missingVar) + { + // Arrange + + ITemplateContext ctx = new TemplateContext() + .ErrorWhenValueMissing(); + //ctx.DefineLocalVariable("e", new ArrayValue(new List())); + ctx.DefineLocalVariable("d", new DictionaryValue(new Dictionary())); + + // Act + var result = RenderingHelper.RenderTemplate("Result : {{ "+varname+" }}", ctx); + + // Assert + Assert.That(result, Is.EqualTo("Result : ERROR: " + missingVar + " is undefined")); + + } + + [Test] + [TestCase("e[1]")] + [TestCase("e.first")] + [TestCase("e.last")] + public void It_Should_Display_An_Error_When_Dereferencing_Empty_Array(String varname) + { + // Arrange + ITemplateContext ctx = new TemplateContext() + .ErrorWhenValueMissing(); + ctx.DefineLocalVariable("e", new ArrayValue(new List())); + + // Act + var result = RenderingHelper.RenderTemplate("Result : {{ " + varname + " }}", ctx); + + // Assert + Assert.That(result, Is.EqualTo("Result : ERROR: cannot dereference empty array")); + + } + + [Test] + public void It_Should_Display_Error_When_Dereferencing_Array_With_Non_Int() + { + // Arrange + ITemplateContext ctx = new TemplateContext() + .ErrorWhenValueMissing(); + ctx.DefineLocalVariable("e", new ArrayValue(new List())); + + // Act + var result = RenderingHelper.RenderTemplate("Result : {{ e.x }}", ctx); + + // Assert + Assert.That(result, Is.StringContaining("invalid index: 'x'")); + + } + + [Test] + [TestCase("x")] + [TestCase("e[1]")] + [TestCase("e.x")] + [TestCase("d.x")] + [TestCase("d[x]")] + [TestCase("a.b.c")] + public void It_Should_Not_Display_An_Error_When_Dereferencing_Missing_Value(String varname) + { + // Arrange + Console.WriteLine(varname); + TemplateContext ctx = new TemplateContext(); + ctx.DefineLocalVariable("e", new ArrayValue(new List())); + ctx.DefineLocalVariable("d", new DictionaryValue(new Dictionary())); + + // Act + var result = RenderingHelper.RenderTemplate("Result : {{ " + varname + " }}", ctx); + + // Assert + Assert.That(result, Is.EqualTo("Result : ")); + + } + + + + } +} diff --git a/Liquid.NET.Tests/Constants/ReflectorTests.cs b/Liquid.NET.Tests/Constants/ReflectorTests.cs new file mode 100755 index 0000000..8a95c63 --- /dev/null +++ b/Liquid.NET.Tests/Constants/ReflectorTests.cs @@ -0,0 +1,58 @@ +using System; +using Liquid.NET.Constants; +using NUnit.Framework; + +namespace Liquid.NET.Tests.Constants +{ + [TestFixture] + public class ReflectorTests + { + [Test] + public void It_Should_Create_A_Dictionary_From_An_Object() + { + // Arrange + var testObj = new TestObj(); + var reflector = new Reflector(); + + // Act + + var exprObject = (DictionaryValue) reflector.GenerateExpressionConstant(testObj); + + // Assert + Assert.That(exprObject, Is.Not.Null); + Assert.That(exprObject.ValueAt("MyProperty").Value, Is.EqualTo(testObj.MyProperty)); + Assert.That(exprObject.ValueAt("MyPropertyWithPrivateSetter").Value, Is.EqualTo(testObj.MyPropertyWithPrivateSetter)); + Assert.That(exprObject.ValueAt("MyPublicField").Value, Is.EqualTo(testObj.MyPublicField)); + Assert.That(exprObject.ValueAt("MyPrivateProperty").HasValue, Is.False); + Assert.That(exprObject.ValueAt("MyPrivateField").HasValue, Is.False); + } + + public class TestObj + { + public TestObj() + { + MyProperty = "A Property"; + MyPropertyWithPrivateSetter = "Property With Private Setter"; + MyPrivateProperty = "A Private Property"; + } + + public String MyPublicField = "a public field"; + +#pragma warning disable 169 +#pragma warning disable 414 + // ReSharper disable once InconsistentNaming + private String MyPrivateField = "a private field"; +#pragma warning restore 414 +#pragma warning restore 169 + + public String MyProperty { get; set; } + + public String MyPropertyWithPrivateSetter { get; private set; } + + private String MyPrivateProperty { get; set; } + + } + + + } +} diff --git a/Liquid.NET.Tests/Expressions/VariableReferenceTests.cs b/Liquid.NET.Tests/Expressions/VariableReferenceTests.cs index 873b4a7..8c88884 100755 --- a/Liquid.NET.Tests/Expressions/VariableReferenceTests.cs +++ b/Liquid.NET.Tests/Expressions/VariableReferenceTests.cs @@ -22,8 +22,23 @@ public void It_Should_Derefence_A_Variable() // Assert Assert.That(result.Value, Is.EqualTo("HELLO")); + } + + [Test] + public void It_Should_Derefence_A_Variable_Missing_Variable_As_None() + { + // Arrange + var variableReference = new VariableReference("myvar"); + var templateContext = new TemplateContext(); + + // Act + var result = variableReference.Eval(templateContext, new List>()); + + // Assert + Assert.That(result.SuccessResult.HasValue, Is.False); } + } } diff --git a/Liquid.NET.Tests/Filters/Array/MapFilterTests.cs b/Liquid.NET.Tests/Filters/Array/MapFilterTests.cs index cc39f88..9e0f541 100755 --- a/Liquid.NET.Tests/Filters/Array/MapFilterTests.cs +++ b/Liquid.NET.Tests/Filters/Array/MapFilterTests.cs @@ -3,6 +3,7 @@ using System.Linq; using Liquid.NET.Constants; using Liquid.NET.Filters.Array; +using Liquid.NET.Utils; using NUnit.Framework; namespace Liquid.NET.Tests.Filters.Array @@ -45,25 +46,7 @@ public void It_Should_Return_None_Where_A_Field_Is_Missing() // Act var result = (mapFilter.Apply(new TemplateContext(), array).SuccessValue()).ToList(); - - - // Assert - //IEnumerable expected = dictionaryValues.Select(x => x.DictValue[field].; -// var expected = array.ArrValue.Select(x => x.Value.ValueAs().DictValue) -// .Select(x => x.ContainsKey(field) ? x[field].Value.ToString() : ""); - - -// var expected = array.ArrValue.Cast().Select( -// x => x.DictValue.ContainsKey(field) ? -// x.DictValue[field].Value.ToString() : -// UndefinedMessage(field)).ToList(); - - //Logger.Log("EXPECTED: " + String.Join(",", expected)); Assert.That(result.Count(x => !x.HasValue), Is.EqualTo(1)); - //var actual = result.Select(x => x.Value.ToString()); - - //Logger.Log("ACTUAL: " + String.Join(",", actual)); - //Assert.That(actual, Is.EquivalentTo(expected)); } @@ -101,10 +84,46 @@ public void It_Should_Do_The_Same_As_Lookup_When_Dictionary() Assert.That(result, Is.EquivalentTo("Value 1 A")); } -// private static string UndefinedMessage(string field) -// { -// return Undefined.CreateUndefinedMessage(field).ToString(); -// } + + [Test] + public void It_Should_Render_The_Fields() + { + // Arrange + var array = CreateArray(); + ITemplateContext ctx = new TemplateContext() + .WithAllFilters().DefineLocalVariable("arr", array); + var result = RenderingHelper.RenderTemplate("Result : {{ arr | map: \"field1\" }}",ctx); + + // Assert + Assert.That(result, Is.EqualTo("Result : Value 1 AValue 2 AValue 3 AValue 4 A")); + } + + [Test] + public void It_Should_Render_Missing_Fields_When_ErrorsOff() + { + // Arrange + var array = CreateArray(); + ITemplateContext ctx = new TemplateContext() + .WithAllFilters().DefineLocalVariable("arr", array); + var result = RenderingHelper.RenderTemplate("Result : {{ arr | map: \"awefwef\" }}", ctx); + + // Assert + Assert.That(result, Is.EqualTo("Result : ")); + } + + [Test] + public void It_Should_Error_When_No_Field_When_ErrorsOn() + { + // Arrange + var array = CreateArray(); + ITemplateContext ctx = new TemplateContext() + .ErrorWhenValueMissing() + .WithAllFilters().DefineLocalVariable("arr", array); + var result = RenderingHelper.RenderTemplate("Result : {{ arr | map: \"missing\" }}", ctx); + Console.WriteLine("Result "+result); + // Assert + Assert.That(result, Is.StringContaining("missing is undefined")); + } public ArrayValue CreateArray() { diff --git a/Liquid.NET.Tests/Filters/Array/SortFilterTests.cs b/Liquid.NET.Tests/Filters/Array/SortFilterTests.cs index a1ef8a7..a977cbd 100755 --- a/Liquid.NET.Tests/Filters/Array/SortFilterTests.cs +++ b/Liquid.NET.Tests/Filters/Array/SortFilterTests.cs @@ -38,13 +38,7 @@ public void It_Should_Sort_An_Array_By_StringValues() public void It_Should_Sort_Dictionaries_By_Field() { // Arrange - IList objlist = new List - { - DataFixtures.CreateDictionary(1, "Aa", "Value 1 B"), - DataFixtures.CreateDictionary(2, "Z", "Value 2 B"), - DataFixtures.CreateDictionary(3, "ab", "Value 3 B"), - DataFixtures.CreateDictionary(4, "b", "Value 4 B"), - }; + IList objlist = CreateObjList(); ArrayValue arrayValue = new ArrayValue(objlist); SortFilter sizeFilter = new SortFilter(new StringValue("field1")); @@ -56,7 +50,60 @@ public void It_Should_Sort_Dictionaries_By_Field() Assert.That(IdAt(result.SuccessValue(), 1, "field1").Value, Is.EqualTo("ab")); Assert.That(IdAt(result.SuccessValue(), 2, "field1").Value, Is.EqualTo("b")); Assert.That(IdAt(result.SuccessValue(), 3, "field1").Value, Is.EqualTo("Z")); + } + [Test] + public void It_Should_Sort_Dictionaries_By_Field_From_Template() + { + // Arrange + ITemplateContext ctx = new TemplateContext() + .WithAllFilters().DefineLocalVariable("arr", new ArrayValue(CreateObjList())); + + // Act + var result = RenderingHelper.RenderTemplate("Result : {% assign x = arr | sort: \"field1\" %}{{ x | map: \"field1\" }}", ctx); + + // Assert + Assert.That(result, Is.EqualTo("Result : AaabbZ")); + } + + [Test] + public void It_Should_Ignore_Dictionaries_With_Missing_Fields() + { + // Arrange + ITemplateContext ctx = new TemplateContext() + .WithAllFilters().DefineLocalVariable("arr", new ArrayValue(CreateObjList())); + + // Act + var result = RenderingHelper.RenderTemplate("Result : {% assign x = arr | sort: \"test\" %}{{ x | map: \"id\" }}", ctx); + + // Assert + Assert.That(result, Is.EqualTo("Result : 1234")); + } + + [Test] + public void It_Should_Error_With_Dictionaries_With_Missing_Fields_When_Errors_On() + { + // Arrange + ITemplateContext ctx = new TemplateContext().ErrorWhenValueMissing() + .WithAllFilters().DefineLocalVariable("arr", new ArrayValue(CreateObjList())); + + // Act + var result = RenderingHelper.RenderTemplate("Result : {% assign x = arr | sort: \"test\" %}", ctx); + + // Assert + Assert.That(result, Is.StringContaining("an array element is missing the field \'test\'")); + } + + + private IList CreateObjList() + { + return new List + { + DataFixtures.CreateDictionary(1, "Aa", "Value 1 B"), + DataFixtures.CreateDictionary(2, "Z", "Value 2 B"), + DataFixtures.CreateDictionary(3, "ab", "Value 3 B"), + DataFixtures.CreateDictionary(4, "b", "Value 4 B"), + }; } private static IExpressionConstant IdAt(ArrayValue result, int index, String field) diff --git a/Liquid.NET.Tests/Filters/Strings/RemoveFilterTests.cs b/Liquid.NET.Tests/Filters/Strings/RemoveFilterTests.cs index f1cc693..f7384be 100755 --- a/Liquid.NET.Tests/Filters/Strings/RemoveFilterTests.cs +++ b/Liquid.NET.Tests/Filters/Strings/RemoveFilterTests.cs @@ -48,7 +48,7 @@ public void It_Should_Remove_Nil_From_A_String() var result = RenderingHelper.RenderTemplate("Result : {{ \"test\" | remove : x }}"); // Assert - Assert.That(result, Is.EqualTo("Result : Please specify a replacement string.")); + Assert.That(result, Is.EqualTo("Result : ERROR: Please specify a replacement string.")); } diff --git a/Liquid.NET.Tests/Liquid.NET.Tests.csproj b/Liquid.NET.Tests/Liquid.NET.Tests.csproj index 38d8e8a..68010af 100755 --- a/Liquid.NET.Tests/Liquid.NET.Tests.csproj +++ b/Liquid.NET.Tests/Liquid.NET.Tests.csproj @@ -64,6 +64,7 @@ + diff --git a/Liquid.NET.Tests/Ruby/FilterTests.cs b/Liquid.NET.Tests/Ruby/FilterTests.cs index 4f2c9de..352dfad 100755 --- a/Liquid.NET.Tests/Ruby/FilterTests.cs +++ b/Liquid.NET.Tests/Ruby/FilterTests.cs @@ -55,6 +55,9 @@ public class FilterTests { [TestCase(@"{{ """" | append: x }}", @"", @"")] [TestCase(@"{{ nil | default: ""NIL TEST"" }}", @"", @"NIL TEST")] [TestCase(@"{{ """" | default: ""ES TEST"" }}", @"", @"ES TEST")] + [TestCase(@"{{ nil | map: ""test"" }}", @"", @"")] + [TestCase(@"{% assign myarray = ""1,2,3,4"" |split: "","" %}{{ myarray.x }}", @"", @"")] + [TestCase(@"{% assign myarray = ""1,2,3,4"" |split: "","" %}{{ myarray[x] }}", @"", @"")] public void It_Should_Match_Ruby_Output(String input, String assigns, String expected) { // Arrange diff --git a/Liquid.NET.Tests/Symbols/SymbolTableStackTests.cs b/Liquid.NET.Tests/Symbols/SymbolTableStackTests.cs index 34ee849..79ea834 100755 --- a/Liquid.NET.Tests/Symbols/SymbolTableStackTests.cs +++ b/Liquid.NET.Tests/Symbols/SymbolTableStackTests.cs @@ -47,7 +47,7 @@ public void It_Should_Retrieve_A_Defined_Local_Registry_Value() [Test] - public void It_Should_Retrieve_An_Undefined_Value_When_Missing() + public void It_Should_Retrieve_An_Error_When_Missing() { // Arrange var stack = StackHelper.CreateSymbolTableStack(); @@ -57,7 +57,7 @@ public void It_Should_Retrieve_An_Undefined_Value_When_Missing() // Assert Assert.That(result, Is.Not.Null); - Assert.That(result.SuccessResult.HasValue, Is.False); + Assert.That(result.IsError, Is.True); //Assert.That(result, Is.TypeOf()); } diff --git a/Liquid.NET/Liquid.NET.csproj b/Liquid.NET/Liquid.NET.csproj index af1fe0e..c6b4301 100755 --- a/Liquid.NET/Liquid.NET.csproj +++ b/Liquid.NET/Liquid.NET.csproj @@ -80,6 +80,7 @@ + diff --git a/Liquid.NET/src/Constants/ArrayValue.cs b/Liquid.NET/src/Constants/ArrayValue.cs index 4922135..e6c4299 100755 --- a/Liquid.NET/src/Constants/ArrayValue.cs +++ b/Liquid.NET/src/Constants/ArrayValue.cs @@ -63,7 +63,7 @@ public override string ToString() //return "[ " + String.Join(", ", strs) + " ]"; // The Concatenated way: - var strs = ArrValue.Select(x => x.Value.ToString()); + var strs = ArrValue.Where(x => x.HasValue).Select(x => x.Value.ToString()); return String.Join("", strs); } } diff --git a/Liquid.NET/src/Constants/DictionaryValue.cs b/Liquid.NET/src/Constants/DictionaryValue.cs index 0faf47c..00891b5 100755 --- a/Liquid.NET/src/Constants/DictionaryValue.cs +++ b/Liquid.NET/src/Constants/DictionaryValue.cs @@ -27,11 +27,9 @@ public override object Value public IDictionary> DictValue { get { return _value; } } - // TODO: not sure what this should do public override bool IsTrue { get { return _value != null; } - //get { return _value.Keys.Any(); } } public override string LiquidTypeName @@ -39,9 +37,19 @@ public override string LiquidTypeName get { return "hash"; } } + /// + /// This will return None if the key is missing, even if ErrorOnMissing is enabled. + /// + /// + /// public Option ValueAt(String key) { - return _value.ContainsKey(key) ? _value[key] : new None(); + return ContainsKey(key) ? _value[key] : new None(); + } + + public bool ContainsKey(string key) + { + return _value.ContainsKey(key); } public override string ToString() @@ -57,7 +65,7 @@ public override string ToString() private static String FormatKvPair(string key, Option expressionConstant) { Type wrappedType = GetWrappedType(expressionConstant); - String exprConstantAsString = expressionConstant.HasValue? expressionConstant.Value.ToString() : "null"; + String exprConstantAsString = expressionConstant.HasValue ? expressionConstant.Value.ToString() : "null"; return Quote(typeof(StringValue), key) + " : " + Quote(wrappedType, exprConstantAsString); } diff --git a/Liquid.NET/src/Constants/FieldAccessor.cs b/Liquid.NET/src/Constants/FieldAccessor.cs index 89fa3b5..92276b9 100755 --- a/Liquid.NET/src/Constants/FieldAccessor.cs +++ b/Liquid.NET/src/Constants/FieldAccessor.cs @@ -4,15 +4,22 @@ namespace Liquid.NET.Constants { public static class FieldAccessor { - public static Option TryField(IExpressionConstant expressionConstant, string stringVal) + public static LiquidExpressionResult TryField( + ITemplateContext ctx, + IExpressionConstant expressionConstant, + string index) { + var dict = expressionConstant as DictionaryValue; if (dict == null) { - return new None(); - + return LiquidExpressionResult.ErrorOrNone(ctx, index); + } - return dict.DictValue.ContainsKey(stringVal) ? dict.DictValue[stringVal] : new None(); + + return dict.DictValue.ContainsKey(index) + ? LiquidExpressionResult.Success(dict.DictValue[index]) + : LiquidExpressionResult.ErrorOrNone(ctx, index); } } } diff --git a/Liquid.NET/src/Constants/IndexDereferencer.cs b/Liquid.NET/src/Constants/IndexDereferencer.cs index 892fe88..22fe8c6 100755 --- a/Liquid.NET/src/Constants/IndexDereferencer.cs +++ b/Liquid.NET/src/Constants/IndexDereferencer.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Liquid.NET.Symbols; using Liquid.NET.Utils; namespace Liquid.NET.Constants @@ -41,15 +42,26 @@ public LiquidExpressionResult Lookup( private LiquidExpressionResult DoLookup(ITemplateContext ctx, ArrayValue arrayValue, IExpressionConstant indexProperty) { + bool errorOnEmpty = ctx.Options.ErrorWhenValueMissing && arrayValue.ArrValue.Count == 0; + + String propertyNameString = ValueCaster.RenderAsString(indexProperty); int index; if (propertyNameString.ToLower().Equals("first")) { + if (errorOnEmpty) + { + return LiquidExpressionResult.Error("cannot dereference empty array"); + } index = 0; } else if (propertyNameString.ToLower().Equals("last")) { + if (errorOnEmpty) + { + return LiquidExpressionResult.Error("cannot dereference empty array"); + } index = arrayValue.ArrValue.Count - 1; } else if (propertyNameString.ToLower().Equals("size")) @@ -58,22 +70,39 @@ private LiquidExpressionResult DoLookup(ITemplateContext ctx, ArrayValue arrayVa } else { - var maybeIndexResult = ValueCaster.Cast(indexProperty); - if (maybeIndexResult.IsError || !maybeIndexResult.SuccessResult.HasValue) - { - return LiquidExpressionResult.Error("invalid array index: " + propertyNameString); - } - else + var success = Int32.TryParse(propertyNameString, out index); + //var maybeIndexResult = ValueCaster.Cast(indexProperty); + + if (!success) { - index = maybeIndexResult.SuccessValue().IntValue; + if (ctx.Options.ErrorWhenValueMissing) + { + return LiquidExpressionResult.Error("invalid index: '" + propertyNameString + "'"); + } + else + { + return LiquidExpressionResult.Success(new None());// liquid seems to return nothing when non-int index. + } } + +// if (maybeIndexResult.IsError || !maybeIndexResult.SuccessResult.HasValue) +// { +// return LiquidExpressionResult.Error("invalid array index: " + propertyNameString); +// } +// else +// { +// index = maybeIndexResult.SuccessValue().IntValue; +// } } if (arrayValue.ArrValue.Count == 0) { - return LiquidExpressionResult.Success(new None()); // not an error in Ruby liquid. + return errorOnEmpty ? + LiquidExpressionResult.Error("cannot dereference empty array") : + LiquidExpressionResult.Success(new None()); } var result = arrayValue.ValueAt(index); + return LiquidExpressionResult.Success(result); } @@ -86,7 +115,16 @@ private LiquidExpressionResult DoLookup(ITemplateContext ctx, DictionaryValue di return LiquidExpressionResult.Success(NumericValue.Create(dictionaryValue.DictValue.Keys.Count)); } - return LiquidExpressionResult.Success(dictionaryValue.ValueAt(indexProperty.Value.ToString())); + var valueAt = dictionaryValue.ValueAt(indexProperty.Value.ToString()); + if (valueAt.HasValue) + { + return LiquidExpressionResult.Success(valueAt); + } + else + { + return LiquidExpressionResult.ErrorOrNone(ctx, indexProperty.ToString()); + + } } // TODO: this is inefficient and ugly and duplicates much of ArrayValue diff --git a/Liquid.NET/src/Constants/MissingValueTest.cs b/Liquid.NET/src/Constants/MissingValueTest.cs new file mode 100755 index 0000000..2d95901 --- /dev/null +++ b/Liquid.NET/src/Constants/MissingValueTest.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Liquid.NET.src.Constants +{ + class MissingValueTest + { + } +} diff --git a/Liquid.NET/src/Expressions/VariableReference.cs b/Liquid.NET/src/Expressions/VariableReference.cs index 991a9e5..5c5d798 100755 --- a/Liquid.NET/src/Expressions/VariableReference.cs +++ b/Liquid.NET/src/Expressions/VariableReference.cs @@ -18,9 +18,22 @@ public VariableReference(String name) public override LiquidExpressionResult Eval(ITemplateContext templateContext, IEnumerable> childresults) { - return templateContext.SymbolTableStack.Reference(Name); + var lookupResult= templateContext.SymbolTableStack.Reference(Name); + return lookupResult.IsSuccess ? + lookupResult : + ErrorOrNone(templateContext, lookupResult); } - + private LiquidExpressionResult ErrorOrNone(ITemplateContext templateContext, LiquidExpressionResult failureResult) + { + if (templateContext.Options.ErrorWhenValueMissing) + { + return failureResult; + } + else + { + return LiquidExpressionResult.Success(new None()); + } + } } } diff --git a/Liquid.NET/src/Expressions/VariableReferenceTree.cs b/Liquid.NET/src/Expressions/VariableReferenceTree.cs index 534c7f1..a3ff606 100755 --- a/Liquid.NET/src/Expressions/VariableReferenceTree.cs +++ b/Liquid.NET/src/Expressions/VariableReferenceTree.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Liquid.NET.Constants; +using Liquid.NET.Symbols; using Liquid.NET.Utils; namespace Liquid.NET.Expressions @@ -22,7 +23,6 @@ public class VariableReferenceTree : IExpressionDescription public LiquidExpressionResult Eval(ITemplateContext templateContext, IEnumerable> childresults) { - //return EvalExpression(templateContext, (dynamic) this, childresults); return EvalExpression(templateContext, this, childresults); } @@ -53,7 +53,7 @@ private LiquidExpressionResult EvalExpression( if (!valueResult.SuccessResult.HasValue) { return LiquidExpressionResult.Success(new None()); - //return LiquidExpressionResult.Error("ERROR: there is no value"); + //return LiquidExpressionResult.Error(SymbolTable.NotFoundError(valueResult)); } if (!indexResult.SuccessResult.HasValue) { diff --git a/Liquid.NET/src/Filters/Array/MapFilter.cs b/Liquid.NET/src/Filters/Array/MapFilter.cs index e791043..58bd5b5 100755 --- a/Liquid.NET/src/Filters/Array/MapFilter.cs +++ b/Liquid.NET/src/Filters/Array/MapFilter.cs @@ -1,11 +1,12 @@ using System; using System.Linq; +using System.Security.Cryptography.X509Certificates; using Liquid.NET.Constants; using Liquid.NET.Utils; namespace Liquid.NET.Filters.Array { - public class MapFilter: FilterExpression// : FilterExpression + public class MapFilter: FilterExpression { private readonly StringValue _selector; @@ -22,24 +23,20 @@ public override LiquidExpressionResult ApplyTo(ITemplateContext ctx, IExpression public override LiquidExpressionResult ApplyTo(ITemplateContext ctx, ArrayValue liquidArrayExpression) { - if (liquidArrayExpression == null || liquidArrayExpression.Value == null) - { - return LiquidExpressionResult.Error("Array is nil"); - } - var list = liquidArrayExpression.ArrValue.Select(x => x.HasValue ? - FieldAccessor.TryField(x.Value, _selector.StringVal) : - new None()).ToList(); - return LiquidExpressionResult.Success(new ArrayValue(list)); + var list = liquidArrayExpression.ArrValue.Select(x => x.HasValue + ? FieldAccessor.TryField(ctx, x.Value, _selector.StringVal) + : LiquidExpressionResult.ErrorOrNone(ctx, _selector.StringVal)).ToList(); + //new None()).ToList(); + return list.Any(x => x.IsError) ? + list.First(x => x.IsError) : + LiquidExpressionResult.Success(new ArrayValue(list.Select(x => x.SuccessResult).ToList())); } public override LiquidExpressionResult ApplyTo(ITemplateContext ctx, DictionaryValue liquidDictionaryValue) { - if (liquidDictionaryValue == null || liquidDictionaryValue.Value == null) - { - return LiquidExpressionResult.Error("Hash is nil"); - } String propertyNameString = ValueCaster.RenderAsString((IExpressionConstant)_selector); - return LiquidExpressionResult.Success(liquidDictionaryValue.ValueAt(propertyNameString)); + + return LiquidExpressionResult.Success(liquidDictionaryValue.ValueAt(propertyNameString)); } diff --git a/Liquid.NET/src/Filters/Array/SortFilter.cs b/Liquid.NET/src/Filters/Array/SortFilter.cs index 8627ccc..43d3797 100755 --- a/Liquid.NET/src/Filters/Array/SortFilter.cs +++ b/Liquid.NET/src/Filters/Array/SortFilter.cs @@ -30,16 +30,20 @@ public override LiquidExpressionResult ApplyTo(ITemplateContext ctx, ArrayValue } else { - return LiquidExpressionResult.Success(SortByProperty(liquidArrayExpression, sortfield)); + return SortByProperty(ctx, liquidArrayExpression, sortfield); } } - private ArrayValue SortByProperty(ArrayValue val, string sortfield) + private LiquidExpressionResult SortByProperty(ITemplateContext ctx, ArrayValue val, string sortfield) { + if (ctx.Options.ErrorWhenValueMissing && + val.ArrValue.Any(x => FieldAccessor.TryField(ctx, x.Value, sortfield).IsError)) + { + return LiquidExpressionResult.Error("an array element is missing the field '" + sortfield + "'"); + } + var ordered = val.ArrValue.OrderBy(x => AsString(ctx, x, sortfield)); - var ordered = val.ArrValue.OrderBy(x => AsString(x, sortfield)); - // TODO: ThenBy - return new ArrayValue(ordered.ToList()); + return LiquidExpressionResult.Success(new ArrayValue(ordered.ToList())); } private static ArrayValue SortAsArrayOfStrings(ArrayValue val) @@ -48,13 +52,19 @@ private static ArrayValue SortAsArrayOfStrings(ArrayValue val) return new ArrayValue(result.ToList()); } - private String AsString(Option x, string field) + private String AsString(ITemplateContext ctx, Option x, string field) { if (!x.HasValue) { return ""; } - return ValueCaster.RenderAsString(FieldAccessor.TryField(x.Value, field)); + + var liquidExpressionResult = FieldAccessor.TryField(ctx, x.Value, field); + if (liquidExpressionResult.IsError || !liquidExpressionResult.SuccessResult.HasValue) + { + return ""; + } + return ValueCaster.RenderAsString(liquidExpressionResult.SuccessResult.Value); } } diff --git a/Liquid.NET/src/RenderingVisitor.cs b/Liquid.NET/src/RenderingVisitor.cs index 1e82b3c..704ab6f 100755 --- a/Liquid.NET/src/RenderingVisitor.cs +++ b/Liquid.NET/src/RenderingVisitor.cs @@ -97,7 +97,22 @@ private void RenderErrors(IEnumerable liquidErrors) private String FormatErrors(IEnumerable liquidErrors) { - return String.Join("; ", liquidErrors.Select(x => x.Message)); + //return "ERROR: " + String.Join("; ", liquidErrors.Select(x => x.Message)); + return String.Join("; ", liquidErrors.Select(x => FormatError(x))); + } + + private static string FormatError(LiquidError x) + { + // to remain backwards-compatible, this leaves "Liquid Error:" alone. + if (x.Message.IndexOf("Liquid error") >= 0) + { + return x.Message; + } + else + { + return "ERROR: " + x.Message; + } + } private void RenderErrors(IEnumerable liquidErrors) @@ -378,6 +393,28 @@ public void Visit(RootDocumentNode rootDocumentNode) // noop } +// public void Visit(VariableReference variableReference) +// { +// variableReference.Eval(_templateContext, new List>()); +// } + + public void Visit(StringValue stringValue) + { + AppendTextToCurrentAccumulator(Render(stringValue)); + } + + /// + /// Process the object / filter chain + /// + /// + public void Visit(LiquidExpression liquidExpression) + { + //Console.WriteLine("Visiting Object Expression "); + LiquidExpressionEvaluator.Eval(liquidExpression, new List>(), _templateContext) + .WhenSuccess(x => x.WhenSome(some => AppendTextToCurrentAccumulator(Render(x.Value))) + .WhenNone(() => AppendTextToCurrentAccumulator(""))) + .WhenError(RenderError); + } public void Visit(LiquidExpressionTree liquidExpressionTree) { diff --git a/Liquid.NET/src/Symbols/SymbolTable.cs b/Liquid.NET/src/Symbols/SymbolTable.cs index af2f383..765179a 100755 --- a/Liquid.NET/src/Symbols/SymbolTable.cs +++ b/Liquid.NET/src/Symbols/SymbolTable.cs @@ -180,10 +180,16 @@ public LiquidExpressionResult ReferenceLocalVariable(String key) } else { - return LiquidExpressionResult.Success(new None()); + //return LiquidExpressionResult.Success(new None()); + return LiquidExpressionResult.Error(NotFoundError(key)); } } + public static String NotFoundError(String key) + { + return key + " is undefined"; + } + public Object ReferenceLocalRegistryVariable(string key) { if (HasLocalRegistryVariableReference(key)) diff --git a/Liquid.NET/src/Symbols/SymbolTableStack.cs b/Liquid.NET/src/Symbols/SymbolTableStack.cs index 098f05d..6bb8762 100755 --- a/Liquid.NET/src/Symbols/SymbolTableStack.cs +++ b/Liquid.NET/src/Symbols/SymbolTableStack.cs @@ -39,7 +39,8 @@ public LiquidExpressionResult Reference(String reference, int skiplevels = 0) return _symbolTables[i].ReferenceLocalVariable(reference); } } - return LiquidExpressionResult.Success(new None()); + //return LiquidExpressionResult.Success(new None()); + return LiquidExpressionResult.Error(SymbolTable.NotFoundError(reference)); } diff --git a/Liquid.NET/src/TemplateContext.cs b/Liquid.NET/src/TemplateContext.cs index 0441ecc..5e64c9f 100755 --- a/Liquid.NET/src/TemplateContext.cs +++ b/Liquid.NET/src/TemplateContext.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; + using Liquid.NET.Constants; using Liquid.NET.Filters; using Liquid.NET.Filters.Array; @@ -250,12 +251,17 @@ public ITemplateContext WithNoForLimit() return this; } + public ITemplateContext ErrorWhenValueMissing() + { + _options.ErrorWhenValueMissing = true; + return this; + } } public class LiquidOptions { public bool NoForLimit { get; internal set; } - + public bool ErrorWhenValueMissing { get; set; } } } diff --git a/Liquid.NET/src/Utils/LiquidExpressionResult.cs b/Liquid.NET/src/Utils/LiquidExpressionResult.cs index 7740eef..6c60f88 100755 --- a/Liquid.NET/src/Utils/LiquidExpressionResult.cs +++ b/Liquid.NET/src/Utils/LiquidExpressionResult.cs @@ -1,5 +1,6 @@ using System; using Liquid.NET.Constants; +using Liquid.NET.Symbols; namespace Liquid.NET.Utils { @@ -14,6 +15,18 @@ internal LiquidExpressionResult(Option success) { } + public static LiquidExpressionResult ErrorOrNone(ITemplateContext ctx, String varname) + { + if (ctx.Options.ErrorWhenValueMissing) + { + return Error(SymbolTable.NotFoundError(varname)); + } + else + { + return Success(new None()); + } + } + public LiquidExpressionResult WhenError(Action fn) { if (IsLeft) diff --git a/Liquid.Ruby/tests/filters.txt b/Liquid.Ruby/tests/filters.txt index 95e24e9..345cd34 100755 --- a/Liquid.Ruby/tests/filters.txt +++ b/Liquid.Ruby/tests/filters.txt @@ -60,3 +60,9 @@ {{ nil | default: "NIL TEST" }} # {{ "" | default: "ES TEST" }} +# +{{ nil | map: "test" }} +# +{% assign myarray = "1,2,3,4" |split: "," %}{{ myarray.x }} +# +{% assign myarray = "1,2,3,4" |split: "," %}{{ myarray[x] }} \ No newline at end of file