Skip to content

Instantly share code, notes, and snippets.

@ndan
Last active May 1, 2022 08:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ndan/a9a8a3792b0e14bc8a31a0a3d4240666 to your computer and use it in GitHub Desktop.
Save ndan/a9a8a3792b0e14bc8a31a0a3d4240666 to your computer and use it in GitHub Desktop.
# This is modified version of https://github.com/infinitered/phoenix_base/blob/master/lib/phoenix_base/maybe.ex
#
# Discussion https://elixirforum.com/t/maybe-nil-protection-for-nested-structs/468/8
# The reason behind adding maybe https://elixirforum.com/t/elixir-version-of-a-safe-navigation-operator-navigating-nil-in-maps-structs/6023/7
# Why Access Protocol is not implemented for struct: https://github.com/elixir-lang/elixir/issues/2973
#
defmodule Streamgard.Utils.Maybe do
@moduledoc """
## Example
import Maybe
map = %{city: %{name: "Portland"}}
maybe(map, [:city, :name]) # => "Portland"
maybe(map.city.name) # => "Portland"
maybe(map.city.location) # => nil
# Access nested struct
event = %Event{user: %User{name: "John"}}
maybe(event.user.name) # => "John"
# Returns nil when association is nil
event = %Event{user: nil}
maybe(event.user.name) # => nil
# Raises error when association not loaded
event = %Event{user: %Ecto.Association.NotLoaded{__field__: :user}}
maybe(event.user.name) # => raise %Maybe.NotLoadedError{message: "association :user is not loaded"}
# Raises error for unknown key
event = %Event{user: %User{name: "John"}}
maybe(event.user.email) # => raise %Maybe.UnknownKeyError{message: "key :email doens't exist for %User{name: "John"}}
"""
defmodule NotLoadedError do
defexception [:message]
@impl true
def exception(association_name) do
msg = "association #{inspect(association_name)} is not loaded"
%NotLoadedError{message: msg}
end
end
defmodule UnknownKeyError do
defexception [:message]
@impl true
def exception({key, struct}) do
msg = "key #{inspect(key)} doens't exist for #{inspect(struct)}"
%UnknownKeyError{message: msg}
end
end
defmacro maybe(ast) do
[variable | keys] = extract_keys(ast)
quote do
maybe(var!(unquote(variable)), unquote(keys))
end
end
@spec maybe(struct(), [atom]) :: any | nil
def maybe(nil, _keys), do: nil
def maybe(val, []), do: val
def maybe(%Ecto.Association.NotLoaded{__field__: assoc}, _keys),
do: raise(NotLoadedError, assoc)
def maybe(struct, [h | t]) do
case Map.fetch(struct, h) do
{:ok, value} -> maybe(value, t)
:error -> raise(UnknownKeyError, {h, struct})
end
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment