Skip to content

Commit

Permalink
Fixing nil, blank, empty comparisons (#282)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastienros authored Mar 12, 2021
1 parent 0ef19d3 commit 4c351d1
Show file tree
Hide file tree
Showing 13 changed files with 278 additions and 27 deletions.
8 changes: 8 additions & 0 deletions Fluid.Tests/BinaryExpressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,5 +204,13 @@ public Task OperatorsShouldBeEvaluatedFromRightToLeft(string source, string expe
return CheckAsync(source, expected);
}

[Theory]
[InlineData("1 == 1 or 1 == 2 and 1 == 2", "true")]
[InlineData("1 == 1 and 1 == 2 and 1 == 2 or 1 == 1", "false")]
public Task OperatorsHavePriority(string source, string expected)
{
return CheckAsync(source, expected);
}

}
}
76 changes: 76 additions & 0 deletions Fluid.Tests/ParserTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Fluid.Ast;
using Microsoft.Extensions.Primitives;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace Fluid.Tests
Expand All @@ -16,6 +18,17 @@ private static IReadOnlyList<Statement> Parse(string source)
return template.Statements;
}

private async Task CheckAsync(string source, string expected, Action<TemplateContext> init = null)
{
_parser.TryParse("{% if " + source + " %}true{% else %}false{% endif %}", out var template, out var messages);

var context = new TemplateContext();
init?.Invoke(context);

var result = await template.RenderAsync(context);
Assert.Equal(expected, result);
}

[Fact]
public void ShouldFiltersWithNamedArguments()
{
Expand Down Expand Up @@ -513,5 +526,68 @@ public void ShouldSkipNewLines()

Assert.Equal("true", rendered);
}

[Theory]

[InlineData("'' == p", "false")]
[InlineData("p == ''", "false")]
[InlineData("p != ''", "true")]

[InlineData("p == nil", "true")]
[InlineData("p != nil", "false")]
[InlineData("nil == p", "true")]

[InlineData("p == blank", "true")]
[InlineData("blank == p ", "true")]

[InlineData("empty == blank", "true")]
[InlineData("blank == empty", "true")]

[InlineData("nil == blank", "true")]
[InlineData("blank == nil", "true")]

[InlineData("blank == ''", "true")]
[InlineData("'' == blank", "true")]

[InlineData("nil == ''", "false")]
[InlineData("'' == nil", "false")]

[InlineData("empty == ''", "true")]
[InlineData("'' == empty", "true")]

[InlineData("e == ''", "true")]
[InlineData("'' == e", "true")]

[InlineData("e == blank", "true")]
[InlineData("blank == e", "true")]

[InlineData("empty == nil", "false")]
[InlineData("nil == empty", "false")]

[InlineData("p != nil and p != ''", "false")]
[InlineData("p != '' and p != nil", "false")]

[InlineData("e != nil and e != ''", "false")]
[InlineData("e != '' and e != nil", "false")]

[InlineData("f != nil and f != ''", "true")]
[InlineData("f != '' and f != nil", "true")]

[InlineData("e == nil", "false")]
[InlineData("nil == e", "false")]

[InlineData("e == empty ", "true")]
[InlineData("empty == e ", "true")]

[InlineData("empty == f", "false")]
[InlineData("f == empty", "false")]

[InlineData("p == empty", "false")]
[InlineData("empty == p", "false")]

public Task EmptyShouldEqualToNil(string source, string expected)
{
return CheckAsync(source, expected, t => t.SetValue("e", "").SetValue("f", "hello"));
}
}
}
9 changes: 3 additions & 6 deletions Fluid/Filters/MiscFilters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,14 +217,11 @@ public static ValueTask<FluidValue> Date(FluidValue input, FilterArguments argum

var format = arguments.At(0).ToStringValue();

using (var sb = StringBuilderPool.GetInstance())
{
var result = sb.Builder;
var result = new StringBuilder(64);

ForStrf(value, format, result);
ForStrf(value, format, result);

return new StringValue(result.ToString());
}
return new StringValue(result.ToString());

void ForStrf(DateTimeOffset value, string format, StringBuilder result)
{
Expand Down
4 changes: 2 additions & 2 deletions Fluid/Filters/StringFilters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ public static ValueTask<FluidValue> Slice(FluidValue input, FilterArguments argu
{
if (requestedLength <= 0)
{
return StringValue.Empty;
return BlankValue.Instance;
}

var sourceString = input.ToStringValue();
Expand All @@ -184,7 +184,7 @@ public static ValueTask<FluidValue> Slice(FluidValue input, FilterArguments argu

if (requestedStartIndex < 0 && Math.Abs(requestedStartIndex) > sourceStringLength)
{
return StringValue.Empty;
return BlankValue.Instance;
}

var startIndex = requestedStartIndex < 0 ? Math.Max(sourceStringLength + requestedStartIndex, 0) : Math.Min(requestedStartIndex, sourceStringLength);
Expand Down
24 changes: 20 additions & 4 deletions Fluid/FluidParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public class FluidParser

protected readonly Parser<List<FilterArgument>> ArgumentsList;
protected readonly Parser<Expression> LogicalExpression;
protected readonly Parser<Expression> CombinatoryExpression; // and | or
protected static readonly Deferred<Expression> Primary = Deferred<Expression>();
protected static readonly Deferred<Expression> FilterExpression = Deferred<Expression>();
protected readonly Deferred<List<Statement>> KnownTagsList = Deferred<List<Statement>>();
Expand Down Expand Up @@ -99,8 +100,6 @@ public FluidParser()
.Or(Member.Then<Expression>(x => x))
;

RegisteredOperators["or"] = (a, b) => new OrBinaryExpression(a, b);
RegisteredOperators["and"] = (a, b) => new AndBinaryExpression(a, b);
RegisteredOperators["contains"] = (a, b) => new ContainsBinaryExpression(a, b);
RegisteredOperators["startswith"] = (a, b) => new StartsWithBinaryExpression(a, b);
RegisteredOperators["endswith"] = (a, b) => new EndsWithBinaryExpression(a, b);
Expand All @@ -114,7 +113,18 @@ public FluidParser()

var CaseValueList = Separated(BinaryOr, Primary);

LogicalExpression = Primary.And(ZeroOrMany(Terms.NonWhiteSpace().Then<string>(x => x.ToString()).When(x => RegisteredOperators.ContainsKey(x)).And(Primary)))
CombinatoryExpression = Primary.And(ZeroOrOne(Terms.NonWhiteSpace().Then(x => x.ToString()).When(x => RegisteredOperators.ContainsKey(x)).And(Primary)))
.Then(x =>
{
if (x.Item2.Item1 == null)
{
return x.Item1;
}

return RegisteredOperators[x.Item2.Item1](x.Item1, x.Item2.Item2);
});

LogicalExpression = CombinatoryExpression.And(ZeroOrMany(OneOf(Terms.Text("or"), Terms.Text("and")).And(CombinatoryExpression)))
.Then(x =>
{
if (x.Item2.Count == 0)
Expand All @@ -129,7 +139,13 @@ public FluidParser()
var current = x.Item2[i];
var previous = i == 0 ? x.Item1 : x.Item2[i - 1].Item2;

result = RegisteredOperators[current.Item1](previous, current.Item2);
result = current.Item1 switch
{
"or" => new OrBinaryExpression(previous, current.Item2),
"and" => new AndBinaryExpression(previous, current.Item2),
_ => throw new ParseException()
};

}

return result;
Expand Down
4 changes: 2 additions & 2 deletions Fluid/TemplateContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ public TemplateContext(TemplateOptions options)

LocalScope = new Scope(options.Scope);

LocalScope.SetValue("empty", NilValue.Empty);
LocalScope.SetValue("blank", StringValue.Empty);
LocalScope.SetValue("empty", EmptyValue.Instance);
LocalScope.SetValue("blank", BlankValue.Instance);
CultureInfo = options.CultureInfo;
TimeZone = options.TimeZone;
Now = options.Now;
Expand Down
4 changes: 2 additions & 2 deletions Fluid/Utils/StringBuilderPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace Fluid.Utils
/// </summary>
internal sealed class StringBuilderPool : IDisposable
{
private const int DefaultPoolCapacity = 16 * 1024;
private const int DefaultPoolCapacity = 40 * 1024;
private readonly int _defaultCapacity;

// global pool
Expand All @@ -37,7 +37,7 @@ private StringBuilderPool(ObjectPool<StringBuilderPool> pool, int defaultCapacit
/// <summary>
/// If someone need to create a private pool
/// </summary>
internal static ObjectPool<StringBuilderPool> CreatePool(int size = 16, int capacity = DefaultPoolCapacity)
internal static ObjectPool<StringBuilderPool> CreatePool(int size = 100, int capacity = DefaultPoolCapacity)
{
ObjectPool<StringBuilderPool> pool = null;
pool = new ObjectPool<StringBuilderPool>(() => new StringBuilderPool(pool, capacity), size);
Expand Down
2 changes: 1 addition & 1 deletion Fluid/Values/ArrayValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public override bool ToBooleanValue()

public override decimal ToNumberValue()
{
return 0;
return _value.Length;
}

public FluidValue[] Values => _value;
Expand Down
68 changes: 68 additions & 0 deletions Fluid/Values/BlankValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Globalization;
using System.IO;
using System.Text.Encodings.Web;

namespace Fluid.Values
{
public sealed class BlankValue : FluidValue
{
public static readonly BlankValue Instance = new BlankValue();

private BlankValue()
{
}

public override FluidValues Type => FluidValues.Empty;

public override bool Equals(FluidValue other)
{
if (other == this) return true;
if (other == BooleanValue.False) return true;
if (other == EmptyValue.Instance) return true;
if (other.ToObjectValue() == null) return true;
if (other.Type == FluidValues.String && string.IsNullOrWhiteSpace(other.ToStringValue())) return true;

return false;
}

public override bool ToBooleanValue()
{
return true;
}

public override decimal ToNumberValue()
{
return 0;
}

public override object ToObjectValue()
{
return "";
}

public override string ToStringValue()
{
return "";
}

public override bool IsNil()
{
return true;
}

public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo)
{
}

public override bool Equals(object other)
{
// The is operator will return false if null
return other is NilValue;
}

public override int GetHashCode()
{
return GetType().GetHashCode();
}
}
}
68 changes: 68 additions & 0 deletions Fluid/Values/EmptyValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Globalization;
using System.IO;
using System.Text.Encodings.Web;

namespace Fluid.Values
{
public sealed class EmptyValue : FluidValue
{
public static readonly EmptyValue Instance = new EmptyValue();

private EmptyValue()
{
}

public override FluidValues Type => FluidValues.Empty;

public override bool Equals(FluidValue other)
{
if (other.Type == FluidValues.String && other.ToStringValue() == "") return true;
if (other.Type == FluidValues.Array && other.ToNumberValue() == 0) return true;
if (other == BlankValue.Instance) return true;
if (other == EmptyValue.Instance) return true;
if (other == NilValue.Instance) return false;

return false;
}

public override bool ToBooleanValue()
{
return true;
}

public override decimal ToNumberValue()
{
return 0;
}

public override object ToObjectValue()
{
return "";
}

public override string ToStringValue()
{
return "";
}

public override bool IsNil()
{
return true;
}

public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo)
{
}

public override bool Equals(object other)
{
// The is operator will return false if null
return other is NilValue;
}

public override int GetHashCode()
{
return GetType().GetHashCode();
}
}
}
2 changes: 2 additions & 0 deletions Fluid/Values/FluidValues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
public enum FluidValues
{
Nil,
Empty,
Blank,
Array,
Boolean,
Dictionary,
Expand Down
Loading

0 comments on commit 4c351d1

Please sign in to comment.