diff --git a/README.md b/README.md index e42f8b1..d5b0614 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ by adding `expression` to your list of dependencies in `mix.exs`: ```elixir def deps do [ - {:expression, "~> 3.0.0-rc.0"} + {:expression, "~> 3.0.0-rc.1"} ] end ``` diff --git a/lib/expression/eval.ex b/lib/expression/eval.ex index 1987790..9d1a71b 100644 --- a/lib/expression/eval.ex +++ b/lib/expression/eval.ex @@ -390,8 +390,16 @@ defmodule Expression.Eval do defp case_insensitive_scan(map, key) do downcased = String.downcase(key) - Enum.find_value(map, {:not_found, [key]}, fn {k, v} -> - if is_binary(k) and String.downcase(k) == downcased, do: v - end) + # Wrap matches in a tuple so that falsy values (`false`, `nil`) are not + # mistaken by `find_value/3` for "no match". + match = + Enum.find_value(map, {:error, :not_found}, fn {k, v} -> + if is_binary(k) and String.downcase(k) == downcased, do: {:ok, v} + end) + + case match do + {:ok, value} -> value + {:error, :not_found} -> {:not_found, [key]} + end end end diff --git a/mix.exs b/mix.exs index 43c32d7..0045fe1 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Expression.MixProject do use Mix.Project - @version "3.0.0-rc.0" + @version "3.0.0-rc.1" def project do [ diff --git a/test/expression_test.exs b/test/expression_test.exs index f94be77..ecad346 100644 --- a/test/expression_test.exs +++ b/test/expression_test.exs @@ -325,6 +325,58 @@ defmodule ExpressionTest do }) end + test "case-insensitive lookup of a camelCase key holding false" do + context = %{"contact" => %{"isActive" => false}} + + assert Expression.evaluate("@contact.isactive", context) == {:ok, false} + assert Expression.evaluate("@contact.isActive", context) == {:ok, false} + assert Expression.evaluate("@contact.ISACTIVE", context) == {:ok, false} + end + + test "case-insensitive lookup of a top-level key holding false" do + assert Expression.evaluate("@enabled", %{"Enabled" => false}) == {:ok, false} + end + + test "case-insensitive lookup of false through a multi-level attribute chain" do + assert Expression.evaluate("@a.b.c", %{"A" => %{"B" => %{"C" => false}}}) == {:ok, false} + end + + test "case-insensitive lookup of other falsy values" do + context = %{ + "map" => %{ + "itemCount" => 0, + "emptyTags" => [], + "blankNote" => "", + "zeroRate" => 0.0 + } + } + + assert Expression.evaluate("@map.itemcount", context) == {:ok, 0} + assert Expression.evaluate("@map.emptytags", context) == {:ok, []} + assert Expression.evaluate("@map.blanknote", context) == {:ok, ""} + assert Expression.evaluate("@map.zerorate", context) == {:ok, 0.0} + end + + test "a missing key is still reported as not found alongside a false sibling" do + context = %{"contact" => %{"isActive" => false}} + + assert Expression.evaluate_block("contact.missing", context) == + {:ok, {:not_found, ["missing"]}} + + assert_raise Expression.Error, "attribute is not found: `missing`", fn -> + Expression.evaluate_block!("contact.missing > 0", context) + end + end + + test "a case-insensitively resolved false is usable downstream" do + context = %{"contact" => %{"isActive" => false}} + + assert Expression.evaluate_block!("contact.isactive == false", context) == true + assert Expression.evaluate_as_boolean!("@contact.isactive", context) == false + assert Expression.evaluate!(~S|@IF(contact.isactive, "on", "off")|, context) == "off" + assert Expression.evaluate_as_string!("@contact.isactive", context) == "false" + end + test "delete an element from a map" do assert {:ok, %{"age" => 32}} == Expression.evaluate("@delete(patient, \"gender\")", %{