Last active
May 1, 2022 08:43
-
-
Save ndan/a9a8a3792b0e14bc8a31a0a3d4240666 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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