diff --git a/lib/phoenix_base/maybe.ex b/lib/phoenix_base/maybe.ex new file mode 100644 index 0000000..a649c42 --- /dev/null +++ b/lib/phoenix_base/maybe.ex @@ -0,0 +1,82 @@ +defmodule PhoenixBase.Maybe do + @moduledoc """ + Access nested structs, protected from `nil`. See `maybe/1` or `maybe/2` for + more details. + + ## Example + + import PhoenixBase.Maybe + + map = %{city: %{name: "Portland"}} + maybe(map, [:city, :name]) # => "Portland" + maybe(map.city.name) # => "Portland" + maybe(map.city.location) # => nil + + """ + + @doc """ + Get a value out of a nested map or struct, or return nil. Compiles down to + `maybe/2`. In other words, this: + + maybe(map.city.name) + + Is compiled down to a simple function call to `maybe/2`: + + maybe(map, [:city, :name]) + + ## Examples + + iex> map = %{city: %{name: "Portland"}} + ...> maybe(map.city.name) + "Portland" + + iex> map = %{city: nil} + ...> maybe(map.city.name) + nil + """ + defmacro maybe(ast) do + [variable|keys] = extract_keys(ast) + + quote do + maybe(var!(unquote(variable)), unquote(keys)) + end + end + + @doc """ + Get a value out of a nested map or struct, or return nil. For prettier syntax, + see the `maybe/1` macro. + + ## Examples + + iex> map = %{city: %{name: "Portland"}} + ...> maybe(map, [:city, :name]) + "Portland" + + iex> maybe(%{}, [:city, :name]) + nil + """ + @spec maybe(map, [atom]) :: any | nil + def maybe(nil, _keys), do: nil + def maybe(val, []), do: val + def maybe(map, [h|t]) do + maybe(Map.get(map, h), t) + end + + defp extract_keys(ast, keys \\ []) + defp extract_keys([], keys), do: keys + defp extract_keys({{:., _, args}, _, _}, keys) do + extract_keys(args, keys) + end + defp extract_keys([{{:., _, args}, _, _}|t], keys) do + keys = keys ++ extract_keys(args) + extract_keys(t, keys) + end + defp extract_keys([{:., _, args}|t], keys) do + keys = keys ++ extract_keys(args) + extract_keys(t, keys) + end + defp extract_keys([key|t], keys) do + keys = keys ++ [key] + extract_keys(t, keys) + end +end diff --git a/test/phoenix_base/maybe_test.exs b/test/phoenix_base/maybe_test.exs new file mode 100644 index 0000000..f236c05 --- /dev/null +++ b/test/phoenix_base/maybe_test.exs @@ -0,0 +1,7 @@ +defmodule PhoenixBase.MaybeTest do + use ExUnit.Case + + import PhoenixBase.Maybe + + doctest PhoenixBase.Maybe +end diff --git a/web/web.ex b/web/web.ex index 557107f..f9e603e 100644 --- a/web/web.ex +++ b/web/web.ex @@ -54,6 +54,9 @@ defmodule PhoenixBase.Web do import PhoenixBase.Router.Helpers import PhoenixBase.ErrorHelpers import PhoenixBase.Gettext + + # Import Maybe, as it's common to want nil protection in views + import PhoenixBase.Maybe end end