Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to boolean operators #2

Merged
merged 2 commits into from
Feb 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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