Skip to content

Commit

Permalink
Merge pull request #2 from edgurgel/add-support-to-boolean-operators
Browse files Browse the repository at this point in the history
Add support to boolean operators
  • Loading branch information
edgurgel authored Feb 18, 2017
2 parents 2c1191b + dfcbe9b commit 698d275
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 85 deletions.
79 changes: 53 additions & 26 deletions lib/solid/expression.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,79 @@ defmodule Solid.Expression do
@moduledoc """
Expression evaluation for the following binary operators:
== != > < >= <=
Also combine expressions with `and`, `or`
"""

alias Solid.Argument

iex> Solid.Expression.eval(1, :==, 2)
@doc """
Evaluate a single expression
iex> Solid.Expression.eval({1, :==, 2})
false
iex> Solid.Expression.eval(1, :==, 1)
iex> Solid.Expression.eval({1, :==, 1})
true
iex> Solid.Expression.eval(1, :!=, 2)
iex> Solid.Expression.eval({1, :!=, 2})
true
iex> Solid.Expression.eval(1, :!=, 1)
iex> Solid.Expression.eval({1, :!=, 1})
false
iex> Solid.Expression.eval(1, :<, 2)
iex> Solid.Expression.eval({1, :<, 2})
true
iex> Solid.Expression.eval(1, :<, 1)
iex> Solid.Expression.eval({1, :<, 1})
false
iex> Solid.Expression.eval(1, :>, 2)
iex> Solid.Expression.eval({1, :>, 2})
false
iex> Solid.Expression.eval(2, :>, 1)
iex> Solid.Expression.eval({2, :>, 1})
true
iex> Solid.Expression.eval(1, :>=, 1)
iex> Solid.Expression.eval({1, :>=, 1})
true
iex> Solid.Expression.eval(1, :>=, 0)
iex> Solid.Expression.eval({1, :>=, 0})
true
iex> Solid.Expression.eval(1, :>=, 2)
iex> Solid.Expression.eval({1, :>=, 2})
false
iex> Solid.Expression.eval(1, :<=, 1)
iex> Solid.Expression.eval({1, :<=, 1})
true
iex> Solid.Expression.eval(1, :<=, 0)
iex> Solid.Expression.eval({1, :<=, 0})
false
iex> Solid.Expression.eval(1, :<=, 2)
iex> Solid.Expression.eval({1, :<=, 2})
true
iex> Solid.Expression.eval("Beer Pack", :contains, "Pack")
iex> Solid.Expression.eval({"Beer Pack", :contains, "Pack"})
true
iex> Solid.Expression.eval("Meat", :contains, "Pack")
iex> Solid.Expression.eval({"Meat", :contains, "Pack"})
false
iex> Solid.Expression.eval(["Beer", "Pack"], :contains, "Pack")
iex> Solid.Expression.eval({["Beer", "Pack"], :contains, "Pack"})
true
iex> Solid.Expression.eval(["Meat"], :contains, "Pack")
iex> Solid.Expression.eval({["Meat"], :contains, "Pack"})
false
iex> Solid.Expression.eval(nil, :contains, "Pack")
iex> Solid.Expression.eval({nil, :contains, "Pack"})
false
iex> Solid.Expression.eval("Meat", :contains, nil)
iex> Solid.Expression.eval({"Meat", :contains, nil})
false
"""
@spec eval(term, atom, term) :: boolean
def eval(nil, :contains, _v2), do: false
def eval(_v1, :contains, nil), do: false
def eval(v1, :contains, v2) when is_list(v1), do: v2 in v1
def eval(v1, :contains, v2), do: String.contains?(v1, v2)
def eval(v1, op, v2), do: apply(Kernel, op, [v1, v2])
@spec eval({term, atom, term} | boolean) :: boolean
def eval({nil, :contains, _v2}), do: false
def eval({_v1, :contains, nil}), do: false
def eval({v1, :contains, v2}) when is_list(v1), do: v2 in v1
def eval({v1, :contains, v2}), do: String.contains?(v1, v2)
def eval({v1, op, v2}), do: apply(Kernel, op, [v1, v2])
def eval(boolean), do: boolean

@doc """
Evaluate a list of expressions combined with `or`, `and
"""
@spec eval(list, map) :: boolean
def eval([exp | exps], hash) when is_list(exps) do
Enum.reduce(exps, do_eval(exp,hash), fn
[:bool_and, exp], acc ->
do_eval(exp, hash) and acc
[:bool_or, exp], acc ->
do_eval(exp, hash) or acc
end)
end

defp do_eval({v1, op, v2}, hash) do
v1 = Argument.get(v1, hash)
v2 = Argument.get(v2, hash)
eval({v1, op, v2})
end
defp do_eval(boolean, _hash), do: eval(boolean)
end
9 changes: 2 additions & 7 deletions lib/solid/tag.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule Solid.Tag do
More info: https://shopify.github.io/liquid/tags/control-flow/
"""

alias Solid.{Expression, Argument}
alias Solid.Expression

@doc """
Evaluate a tag and return the condition that succeeded or nil
Expand Down Expand Up @@ -45,10 +45,5 @@ defmodule Solid.Tag do
eval_expression(elsif_exp[:expression], hash)
end

defp eval_expression(bool, _hash) when is_boolean(bool), do: bool
defp eval_expression([arg1, op, arg2], hash) do
v1 = Argument.get(arg1, hash)
v2 = Argument.get(arg2, hash)
Expression.eval(v1, op, v2)
end
defp eval_expression(exps, hash), do: Expression.eval(exps, hash)
end
16 changes: 12 additions & 4 deletions src/solid_parser.peg
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ case Node of
end
`;

if_tag <- open_tag space if expression:expression close_tag text:text `
if_tag <- open_tag space if expression:boolean_expression close_tag text:text `
case Node of
[_, _, _If, Exp, _, Text] -> {if_exp, [Exp, Text]}
end
`;

elsif_tag <- open_tag space elsif expression:expression close_tag text:text `
elsif_tag <- open_tag space elsif expression:boolean_expression close_tag text:text `
case Node of
[_, _, _Elsif, Exp, _, Text] -> {elsif_exp, [Exp, Text]}
end
Expand All @@ -40,7 +40,7 @@ case Node of
end
`;

unless_tag <- open_tag space unless expression:expression close_tag text:text `
unless_tag <- open_tag space unless expression:boolean_expression close_tag text:text `
case Node of
[_, _, _Unless, Exp, _, Text] -> {unless_exp, [Exp, Text]}
end
Expand All @@ -60,11 +60,17 @@ end

expression <- space (argument space operator space argument / boolean) space `
case Node of
[_, [Arg1, _, Op, _, Arg2], _] -> [Arg1, Op, Arg2];
[_, [Arg1, _, Op, _, Arg2], _] -> {Arg1, Op, Arg2};
[_, Bool, _] -> Bool
end
`;

boolean_expression <- expression ((and / or) expression)* `
case Node of
[Exp, Exps] -> [Exp | Exps]
end
`;

filters <- (space "|" space filter (":" space arguments)? )+`
[case N of
[_, _, _, Filter, [_, _, Args]] -> {Filter, Args};
Expand Down Expand Up @@ -123,3 +129,5 @@ boolean <- true / false ~;
true <- 'true' `true`;
false <- 'false' `false`;
null <- 'nil' `nil`;
and <- 'and' `bool_and`;
or <- 'or' `bool_or`;
21 changes: 21 additions & 0 deletions test/expression_test.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
defmodule Solid.ExpressionTest do
use ExUnit.Case, async: true
doctest Solid.Expression
import Solid.Expression

@true_exp {1, :==, 1}
@false_exp {1, :!=, 1}

describe "eval/2" do
test "expressions with 1 boolean" do
exps = [true]
assert eval(exps, %{})
end

test "expressions 'and' booleans" do
exps = [true, [:bool_and, false]]
refute eval(exps, %{})
end

test "expressions 'or' booleans" do
exps = [true, [:bool_or, false]]
assert eval(exps, %{})
end
end
end
100 changes: 54 additions & 46 deletions test/integration/tags_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,67 +6,75 @@ defmodule Solid.Integration.TagsTest do
# assert render("{% Text", %{ "key" => 123 }) == "{% Text"
# end

test "if true expression" do
assert render("{% if 1 == 1 %}True{% endif %} is True", %{ "key" => 123 }) == "True is True"
end
describe "if" do
test "true expression" do
assert render("{% if 1 == 1 %}True{% endif %} is True", %{ "key" => 123 }) == "True is True"
end

test "if false expression" do
assert render("{% if 1 != 1 %}True{% endif %}False?", %{ "key" => 123 }) == "False?"
end
test "false expression" do
assert render("{% if 1 != 1 %}True{% endif %}False?", %{ "key" => 123 }) == "False?"
end

test "if true" do
assert render("{% if true %}True{% endif %} is True", %{ "key" => 123 }) == "True is True"
end
test "true" do
assert render("{% if true %}True{% endif %} is True", %{ "key" => 123 }) == "True is True"
end

test "if false" do
assert render("{% if false %}True{% endif %}False?", %{ "key" => 123 }) == "False?"
end
test "false" do
assert render("{% if false %}True{% endif %}False?", %{ "key" => 123 }) == "False?"
end

test "nested if" do
assert render("{% if 1 == 1 %}{% if 1 != 2 %}True{% endif %}{% endif %} is True", %{ "key" => 123 }) == "True is True"
end
test "boolean expression" do
assert render("{% if 1 != 1 or 3 == 3 %}True{% endif %}", %{ "key" => 123 }) == "True"
end

test "if with object" do
assert render("{% if 1 != 2 %}{{ key }}{% endif %}", %{ "key" => 123 }) == "123"
end
test "nested" do
assert render("{% if 1 == 1 %}{% if 1 != 2 %}True{% endif %}{% endif %} is True", %{ "key" => 123 }) == "True is True"
end

test "else true" do
assert render("{% if 1 == 1 %}True{% else %}False{% endif %} is True", %{ "key" => 123 }) == "True is True"
end
test "with object" do
assert render("{% if 1 != 2 %}{{ key }}{% endif %}", %{ "key" => 123 }) == "123"
end

test "else false" do
assert render("{% if 1 != 1 %}True{% else %}False{% endif %} is False", %{ "key" => 123 }) == "False is False"
end
test "else true" do
assert render("{% if 1 == 1 %}True{% else %}False{% endif %} is True", %{ "key" => 123 }) == "True is True"
end

test "if elsif" do
assert render("{% if 1 != 1 %}if{% elsif 1 == 1 %}elsif{% endif %}") == "elsif"
end
test "else false" do
assert render("{% if 1 != 1 %}True{% else %}False{% endif %} is False", %{ "key" => 123 }) == "False is False"
end

test "unless true expression" do
assert render("{% unless 1 == 1 %}True{% endunless %}False?", %{ "key" => 123 }) == "False?"
test "elsif" do
assert render("{% if 1 != 1 %}if{% elsif 1 == 1 %}elsif{% endif %}") == "elsif"
end
end

test "unless false expression" do
assert render("{% unless 1 != 1 %}True{% endunless %} is True", %{ "key" => 123 }) == "True is True"
end
describe "unless" do
test "true expression" do
assert render("{% unless 1 == 1 %}True{% endunless %}False?", %{ "key" => 123 }) == "False?"
end

test "unless true" do
assert render("{% unless true %}False{% endunless %}False?", %{ "key" => 123 }) == "False?"
end
test "false expression" do
assert render("{% unless 1 != 1 %}True{% endunless %} is True", %{ "key" => 123 }) == "True is True"
end

test "unless false" do
assert render("{% unless false %}True{% endunless %} is True", %{ "key" => 123 }) == "True is True"
end
test "true" do
assert render("{% unless true %}False{% endunless %}False?", %{ "key" => 123 }) == "False?"
end

test "nested unless" do
assert render("{% unless 1 != 1 %}{% unless 1 == 2 %}True{% endunless %}{% endunless %} is True", %{ "key" => 123 }) == "True is True"
end
test "false" do
assert render("{% unless false %}True{% endunless %} is True", %{ "key" => 123 }) == "True is True"
end

test "unless with object" do
assert render("{% unless 1 == 2 %}{{ key }}{% endunless %}", %{ "key" => 123 }) == "123"
end
test "nested" do
assert render("{% unless 1 != 1 %}{% unless 1 == 2 %}True{% endunless %}{% endunless %} is True", %{ "key" => 123 }) == "True is True"
end

test "with object" do
assert render("{% unless 1 == 2 %}{{ key }}{% endunless %}", %{ "key" => 123 }) == "123"
end

test "unless elsif" do
assert render("{% unless 1 == 1 %}unless{% elsif 1 == 1 %}elsif{% endunless %}") == "elsif"
test "elsif" do
assert render("{% unless 1 == 1 %}unless{% elsif 1 == 1 %}elsif{% endunless %}") == "elsif"
end
end
end
4 changes: 2 additions & 2 deletions test/tag_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ defmodule Solid.TagTest do
import Solid.Tag
doctest Solid.Tag

@true_exp [{:value, 1}, :==, {:value, 1}]
@false_exp [{:value, 1}, :!=, {:value, 1}]
@true_exp [{{:value, 1}, :==, {:value, 1}}]
@false_exp [{{:value, 1}, :!=, {:value, 1}}]

describe "Tag.eval/2" do
test "eval if_exp true" do
Expand Down

0 comments on commit 698d275

Please sign in to comment.