From c03527bf0811a7ffd17a15c34fbb59f97fd27749 Mon Sep 17 00:00:00 2001 From: Eduardo Gurgel Date: Mon, 24 Oct 2016 00:45:05 +1300 Subject: [PATCH 1/2] Separate integration tests for tags using describe blocks --- test/integration/tags_test.exs | 96 ++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/test/integration/tags_test.exs b/test/integration/tags_test.exs index 5e6bce4..a0fd535 100644 --- a/test/integration/tags_test.exs +++ b/test/integration/tags_test.exs @@ -6,67 +6,71 @@ 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 "nested" do + assert render("{% if 1 == 1 %}{% if 1 != 2 %}True{% endif %}{% endif %} is True", %{ "key" => 123 }) == "True is True" + end - test "if with object" do - assert render("{% if 1 != 2 %}{{ key }}{% endif %}", %{ "key" => 123 }) == "123" - end + test "with object" do + assert render("{% if 1 != 2 %}{{ key }}{% endif %}", %{ "key" => 123 }) == "123" + end - test "else true" do - assert render("{% if 1 == 1 %}True{% else %}False{% 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 "else false" do - assert render("{% if 1 != 1 %}True{% else %}False{% endif %} is False", %{ "key" => 123 }) == "False is False" - end + test "else false" do + assert render("{% if 1 != 1 %}True{% else %}False{% endif %} is False", %{ "key" => 123 }) == "False is False" + end - test "if elsif" do - assert render("{% if 1 != 1 %}if{% elsif 1 == 1 %}elsif{% endif %}") == "elsif" + test "elsif" do + assert render("{% if 1 != 1 %}if{% elsif 1 == 1 %}elsif{% endif %}") == "elsif" + end end - test "unless true expression" do - assert render("{% unless 1 == 1 %}True{% endunless %}False?", %{ "key" => 123 }) == "False?" - end + describe "unless" do + test "true expression" do + assert render("{% unless 1 == 1 %}True{% endunless %}False?", %{ "key" => 123 }) == "False?" + end - test "unless false expression" do - assert render("{% unless 1 != 1 %}True{% endunless %} is True", %{ "key" => 123 }) == "True is True" - end + test "false expression" do + assert render("{% unless 1 != 1 %}True{% endunless %} is True", %{ "key" => 123 }) == "True is True" + end - test "unless true" do - assert render("{% unless true %}False{% endunless %}False?", %{ "key" => 123 }) == "False?" - end + test "true" do + assert render("{% unless true %}False{% endunless %}False?", %{ "key" => 123 }) == "False?" + end - test "unless false" do - assert render("{% unless false %}True{% 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 "nested unless" do - assert render("{% unless 1 != 1 %}{% unless 1 == 2 %}True{% endunless %}{% endunless %} is True", %{ "key" => 123 }) == "True is True" - end + test "nested" do + assert render("{% unless 1 != 1 %}{% unless 1 == 2 %}True{% endunless %}{% 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 "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 From dfcbe9b223a1f9e0b4f1365fdc51e2718731ab25 Mon Sep 17 00:00:00 2001 From: Eduardo Gurgel Date: Sat, 18 Feb 2017 18:38:16 +1300 Subject: [PATCH 2/2] Add support to boolean expressions --- lib/solid/expression.ex | 79 +++++++++++++++++++++++----------- lib/solid/tag.ex | 9 +--- src/solid_parser.peg | 16 +++++-- test/expression_test.exs | 21 +++++++++ test/integration/tags_test.exs | 4 ++ test/tag_test.exs | 4 +- 6 files changed, 94 insertions(+), 39 deletions(-) diff --git a/lib/solid/expression.ex b/lib/solid/expression.ex index 977e402..f782037 100644 --- a/lib/solid/expression.ex +++ b/lib/solid/expression.ex @@ -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 diff --git a/lib/solid/tag.ex b/lib/solid/tag.ex index 9d525d7..93f30ae 100644 --- a/lib/solid/tag.ex +++ b/lib/solid/tag.ex @@ -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 @@ -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 diff --git a/src/solid_parser.peg b/src/solid_parser.peg index f224d95..6ded3b5 100644 --- a/src/solid_parser.peg +++ b/src/solid_parser.peg @@ -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 @@ -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 @@ -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}; @@ -123,3 +129,5 @@ boolean <- true / false ~; true <- 'true' `true`; false <- 'false' `false`; null <- 'nil' `nil`; +and <- 'and' `bool_and`; +or <- 'or' `bool_or`; diff --git a/test/expression_test.exs b/test/expression_test.exs index f93f71a..b3021aa 100644 --- a/test/expression_test.exs +++ b/test/expression_test.exs @@ -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 diff --git a/test/integration/tags_test.exs b/test/integration/tags_test.exs index a0fd535..19c50cf 100644 --- a/test/integration/tags_test.exs +++ b/test/integration/tags_test.exs @@ -23,6 +23,10 @@ defmodule Solid.Integration.TagsTest do assert render("{% if false %}True{% endif %}False?", %{ "key" => 123 }) == "False?" end + test "boolean expression" do + assert render("{% if 1 != 1 or 3 == 3 %}True{% endif %}", %{ "key" => 123 }) == "True" + end + test "nested" do assert render("{% if 1 == 1 %}{% if 1 != 2 %}True{% endif %}{% endif %} is True", %{ "key" => 123 }) == "True is True" end diff --git a/test/tag_test.exs b/test/tag_test.exs index 3d3e012..b7ebd41 100644 --- a/test/tag_test.exs +++ b/test/tag_test.exs @@ -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