ash_phoenix/lib/ash_phoenix.ex

1024 lines
29 KiB
Elixir
Raw Normal View History

2020-10-21 06:54:35 +13:00
defmodule AshPhoenix do
@moduledoc """
Various helpers and utilities for working with Ash changesets and queries and phoenix.
2020-10-21 06:54:35 +13:00
"""
import AshPhoenix.FormData.Helpers
@doc false
def to_form_error(exception) when is_exception(exception) do
case AshPhoenix.FormData.Error.to_form_error(exception) do
nil ->
nil
{field, message} ->
{field, message, []}
{field, message, vars} ->
{field, message, vars}
list when is_list(list) ->
Enum.map(list, fn item ->
case item do
{field, message} ->
{field, message, []}
{field, message, vars} ->
{field, message, vars}
end
end)
end
end
@doc """
Allows for manually transforming errors to modify or enable error messages in the form.
By default, only errors that implement the `AshPhoenix.FormData.Error` protocol will show
their errors in forms. This is to protect you from showing strange errors to the user. Using
this function, you can intercept those errors (as well as ones that *do* implement the protocol)
and return custom form-ready messages for them.
Example:
2021-07-10 14:30:30 +12:00
```elixir
AshPhoenix.transform_errors(changeset, fn changeset, %MyApp.CustomError{message: message} ->
{:id, "Something went wrong while doing the %{thing}", [thing: "request"]}
end)
# Could potentially be used for translation, although not quite ergonomic yet
defp translate_error(key, msg, vars) do
if vars[:count] do
Gettext.dngettext(MyApp.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(MyApp.Gettext, "errors", msg, opts)
end
2021-07-10 14:30:30 +12:00
end
2021-07-10 14:30:30 +12:00
AshPhoenix.transform_errors(changeset, fn
changeset, %MyApp.CustomError{message: message, field: field} ->
translate_error(field, message, [foo: :bar])
changeset, any_error ->
if AshPhoenix.FormData.Error.impl_for(any_error) do
any_error
|> AshPhoenix.FormData.error.to_form_error()
|> List.wrap()
|> Enum.map(fn {key, msg, vars} ->
translate_error(key, msg, vars)
end)
end
end)
```
"""
@spec transform_errors(
Ash.Changeset.t() | Ash.Query.t(),
(Ash.Query.t() | Ash.Changeset.t(), error :: Ash.Error.t() ->
[{field :: atom, message :: String.t(), substituations :: Keyword.t()}])
) :: Ash.Query.t() | Ash.Changeset.t()
def transform_errors(%Ash.Changeset{} = changeset, transform_errors) do
Ash.Changeset.put_context(changeset, :private, %{
ash_phoenix: %{transform_errors: transform_errors}
})
end
def transform_errors(%Ash.Query{} = changeset, transform_errors) do
Ash.Query.put_context(changeset, :private, %{
ash_phoenix: %{transform_errors: transform_errors}
})
end
@doc """
Gets all errors on a changeset or query.
2021-03-20 11:25:05 +13:00
This honors the `AshPhoenix.FormData.Error` protocol and applies any `transform_errors`.
See `transform_errors/2` for more information.
"""
@spec errors_for(Ash.Changeset.t() | Ash.Query.t(), Keyword.t()) ::
[{atom, {String.t(), Keyword.t()}}] | [String.t()] | map
def errors_for(changeset_or_query, opts \\ []) do
errors =
if hiding_errors?(changeset_or_query) do
[]
else
changeset_or_query.errors
|> Enum.flat_map(&transform_error(changeset_or_query, &1))
|> Enum.filter(fn
error when is_exception(error) ->
AshPhoenix.FormData.Error.impl_for(error)
{_key, _value, _vars} ->
true
_ ->
false
end)
|> Enum.map(fn {field, message, vars} ->
vars =
vars
|> List.wrap()
|> Enum.flat_map(fn {key, value} ->
try do
if is_integer(value) do
[{key, value}]
else
[{key, to_string(value)}]
end
rescue
_ ->
[]
end
end)
{field, {message || "", vars}}
end)
end
case opts[:as] do
raw when raw in [:raw, nil] ->
errors
:simple ->
Map.new(errors, fn {field, {message, vars}} ->
message = replace_vars(message, vars)
{field, message}
end)
:plaintext ->
Enum.map(errors, fn {field, {message, vars}} ->
message = replace_vars(message, vars)
"#{field}: " <> message
end)
end
end
defp replace_vars(message, vars) do
Enum.reduce(vars || [], message, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end
defp transform_error(_query, {_key, _value, _vars} = error), do: error
defp transform_error(query, error) do
case query.context[:private][:ash_phoenix][:transform_errors] do
transformer when is_function(transformer, 2) ->
case transformer.(query, error) do
error when is_exception(error) ->
if AshPhoenix.FormData.Error.impl_for(error) do
List.wrap(AshPhoenix.to_form_error(error))
else
[]
end
{key, value, vars} ->
[{key, value, vars}]
list when is_list(list) ->
Enum.flat_map(list, fn
error when is_exception(error) ->
if AshPhoenix.FormData.Error.impl_for(error) do
List.wrap(AshPhoenix.to_form_error(error))
else
[]
end
{key, value, vars} ->
[{key, value, vars}]
end)
end
nil ->
if AshPhoenix.FormData.Error.impl_for(error) do
List.wrap(AshPhoenix.to_form_error(error))
else
[]
end
end
end
def hide_errors(%Ash.Changeset{} = changeset) do
Ash.Changeset.put_context(changeset, :private, %{ash_phoenix: %{hide_errors: true}})
end
def hide_errors(%Ash.Query{} = query) do
Ash.Query.put_context(query, :private, %{ash_phoenix: %{hide_errors: true}})
end
def hiding_errors?(%Ash.Changeset{} = changeset) do
changeset.context[:private][:ash_phoenix][:hide_errors] == true
end
def hiding_errors?(%Ash.Query{} = query) do
query.context[:private][:ash_phoenix][:hide_errors] == true
end
def set_up_relationship_updates(changeset, relationships) when is_list(relationships) do
Enum.reduce(relationships, changeset, &set_up_relationship_updates(&2, &1))
end
@add_value_opts [
add: [
type: :any,
doc: "the value to add to the list",
default: nil
]
]
@doc """
A utility to support "add" buttons on list attributes and arguments used in forms.
To use, simply pass in the form name of the attribute/argument form as well as the name of the primary/outer form.
```elixir
# In your template, inside a form called `:change`
<button phx-click="append_thing" phx-value-path={{form.path <> "[attribute_name]"}}>
</button>
# In the view/component
def handle_event("append_thing", %{"path" => path}, socket) do
changeset = add_value(socket.assigns.changeset, path, "change")
{:noreply, assign(socket, changeset: changeset)}
end
```
## Options
2021-07-10 14:30:30 +12:00
#{Ash.OptionsHelpers.docs(@add_value_opts)}
"""
@spec add_value(Ash.Changeset.t(), String.t(), String.t(), Keyword.t()) :: Ash.Changeset.t()
def add_value(changeset, original_path, outer_form_name, opts \\ []) do
opts = Ash.OptionsHelpers.validate!(opts, @add_value_opts)
add = opts[:add]
[^outer_form_name, key | path] = decode_path(original_path)
if match?({x, y} when not is_nil(x) and not is_nil(y), argument_and_manages(changeset, key)) do
add_related(changeset, original_path, outer_form_name, add: add)
else
attribute_or_argument =
Ash.Resource.Info.attribute(changeset.resource, key) ||
find_argument(changeset, key)
if attribute_or_argument do
value =
case attribute_or_argument do
%Ash.Resource.Actions.Argument{} = argument ->
changeset
|> Ash.Changeset.get_argument(argument.name)
attribute ->
changeset
|> Ash.Changeset.get_attribute(attribute.name)
end
new_value = add_to_path(value, path, add)
case attribute_or_argument do
%Ash.Resource.Actions.Argument{} = argument ->
Ash.Changeset.set_argument(changeset, argument.name, new_value)
attribute ->
Ash.Changeset.change_attribute(changeset, attribute.name, new_value)
end
else
changeset
end
end
end
@doc """
A utility to support "remove" buttons on list attributes and arguments used in forms.
"""
@spec remove_value(Ash.Changeset.t(), String.t(), String.t()) :: Ash.Changeset.t()
def remove_value(changeset, original_path, outer_form_name) do
[^outer_form_name, key | path] = decode_path(original_path)
attribute_or_argument =
Ash.Resource.Info.attribute(changeset.resource, key) ||
find_argument(changeset, key)
if match?({x, y} when not is_nil(x) and not is_nil(y), argument_and_manages(changeset, key)) do
{_, changeset} = remove_related(changeset, original_path, outer_form_name)
changeset
else
if attribute_or_argument do
value =
case attribute_or_argument do
%Ash.Resource.Actions.Argument{} = argument ->
changeset
|> Ash.Changeset.get_argument(argument.name)
|> List.wrap()
attribute ->
changeset
|> Ash.Changeset.get_attribute(attribute.name)
|> List.wrap()
end
new_value = remove_from_path(value, path)
case attribute_or_argument do
%Ash.Resource.Actions.Argument{} = argument ->
Ash.Changeset.set_argument(changeset, argument.name, new_value)
attribute ->
Ash.Changeset.change_attribute(changeset, attribute.name, new_value)
end
else
changeset
end
end
end
defp find_argument(changeset, key) do
changeset.action && Enum.find(changeset.action.arguments, &(to_string(&1.name) == key))
end
@add_related_opts [
use_data?: [
type: :boolean,
doc: "Provide this if you are using `use_data?` with `inputs_for` for this relationship",
default: false
],
add: [
type: :any,
2021-07-10 14:30:30 +12:00
doc: "the value to add to the relationship",
default: %{}
],
relationship: [
type: :atom,
doc: "The relationship being updated, in case it can't be determined from the path"
],
id: [
type: :any,
doc:
"The value that should be in `meta[:id]` in the manage changeset opts. Defaults to the relationship name. This only needs to be set if an id is also provided for `inputs_for`."
]
]
@doc """
A utility to support "add" buttons on relationships used in forms.
To use, simply pass in the form name of the relationship form as well as the name of the primary/outer form.
```elixir
# In your template, inside a form called `:change`
<button phx-click="append_thing" phx-value-path={{form.path}}>
</button>
# In the view/component
def handle_event("append_thing", %{"path" => path}, socket) do
changeset = add_related(socket.assigns.changeset, path, "change")
{:noreply, assign(socket, changeset: changeset)}
end
```
## Options
2021-07-10 14:30:30 +12:00
#{Ash.OptionsHelpers.docs(@add_related_opts)}
"""
@spec add_related(Ash.Changeset.t(), String.t(), String.t(), Keyword.t()) :: Ash.Changeset.t()
def add_related(changeset, original_path, outer_form_name, opts \\ []) do
opts = Ash.OptionsHelpers.validate!(opts, @add_related_opts)
add = Keyword.get(opts, :add, %{})
[^outer_form_name, key | path] =
if is_list(original_path) do
original_path
else
decode_path(original_path)
end
{argument, argument_manages} = argument_and_manages(changeset, key)
changeset.resource
|> Ash.Resource.Info.relationships()
|> Enum.find_value(fn relationship ->
if relationship.name == opts[:relationship] || to_string(relationship.name) == key ||
relationship.name == argument_manages do
manage = changeset.relationships[relationship.name] || []
to_manage =
Enum.find_index(manage, fn {_manage, opts} ->
opts[:id] == opts[:key] || opts[:id] == opts[:relationship] ||
(argument && opts[:id] == argument.name)
end)
{relationship.name,
opts[:id] || opts[:relationship] || (argument && argument.name) || relationship.name,
to_manage}
end
end)
|> case do
nil ->
changeset
{rel, id, nil} ->
2021-07-13 09:37:06 +12:00
new_relationships =
changeset.relationships
|> Map.put_new(rel, [])
|> Map.update!(rel, fn manages ->
to_add =
if opts[:use_data?] && loaded?(changeset.data, rel) do
changeset.data
|> Map.get(rel)
|> List.wrap()
|> Enum.reduce(nil, fn _, acc ->
add_to_path(acc, path, %{})
end)
|> add_to_path(path, add)
else
add_to_path(nil, path, add)
end
2021-07-13 09:37:06 +12:00
manages ++ [{to_add, [meta: [id: id]]}]
end)
%{changeset | relationships: new_relationships}
{rel, _id, index} ->
2021-07-13 09:37:06 +12:00
new_relationships =
changeset.relationships
|> Map.put_new(rel, [])
|> Map.update!(rel, fn manages ->
List.update_at(manages, index, fn {manage, opts} ->
{add_to_path(manage, path, add), opts}
end)
2021-07-13 09:37:06 +12:00
end)
2021-07-13 09:37:06 +12:00
%{changeset | relationships: new_relationships}
end
end
defp loaded?(data, key) do
case Map.get(data, key) do
%Ash.NotLoaded{} -> false
_ -> true
end
end
@remove_related_opts [
add: [
type: :any,
doc: "the value to add to the relationship, defaults to `%{}`",
default: %{}
],
relationship: [
type: :atom,
doc: "The relationship being updated, in case it can't be determined from the path"
],
id: [
type: :any,
doc:
"The value that should be in `meta[:id]` in the manage changeset opts. Defaults to the relationship name. This only needs to be set if an id is also provided for `inputs_for`."
]
]
@doc """
A utility to support "remove" buttons on relationships used in forms.
To use, simply pass in the form name of the related form as well as the name of the primary/outer form.
```elixir
# In your template, inside a form called `:change`
<button phx-click="remove_thing" phx-value-path={{form.path}}>
</button>
# In the view/component
def handle_event("remove_thing", %{"path" => path}, socket) do
{record, changeset} = remove_related(socket.assigns.changeset, path, "change")
{:noreply, assign(socket, record: record, changeset: changeset)}
end
```
## Options
2021-07-10 14:30:30 +12:00
#{Ash.OptionsHelpers.docs(@remove_related_opts)}
"""
@spec remove_related(Ash.Changeset.t(), String.t(), String.t(), Keyword.t()) ::
{Ash.Resource.record(), Ash.Changeset.t()}
def remove_related(changeset, path, outer_form_name, opts \\ []) do
[^outer_form_name, key | path] = decode_path(path)
{argument, argument_manages} = argument_and_manages(changeset, key)
changeset.resource
|> Ash.Resource.Info.relationships()
|> Enum.find_value(fn relationship ->
if relationship.name == opts[:relationship] || to_string(relationship.name) == key ||
relationship.name == argument_manages do
manage = changeset.relationships[relationship.name] || []
to_manage =
Enum.find_index(manage, fn {_manage, opts} ->
opts[:id] == opts[:key] || opts[:id] == opts[:relationship] ||
(argument && opts[:id] == argument.name)
end)
{relationship.name, to_manage}
end
end)
|> case do
nil ->
{changeset.data, changeset}
{rel, index} ->
{changeset, index} =
if index == nil do
{%{changeset | relationships: Map.put(changeset.relationships, rel, [])}, 0}
else
{changeset, index}
end
new_relationships =
changeset.relationships
|> Map.put_new(rel, [])
|> Map.update!(rel, fn manages ->
if path == [] do
List.delete_at(manages, index)
else
List.update_at(manages, index, fn {manage, opts} ->
{remove_from_path(manage, path), opts}
end)
end
end)
new_value =
cond do
path == [] ->
nil
is_nil(changeset.relationships[rel]) ->
nil
true ->
case Enum.at(changeset.relationships[rel], index) do
nil ->
nil
{value, _opts} ->
value
end
end
changeset = %{changeset | relationships: new_relationships}
{new_data, new_value} =
if changeset.action_type in [:destroy, :update] do
case Map.get(changeset.data, rel) do
%Ash.NotLoaded{} ->
{[], new_value}
value ->
cond do
path == [] and is_list(value) ->
{Map.update!(changeset.data, rel, fn related ->
Enum.map(related, &hide/1)
end), []}
match?([i | _] when is_integer(i), path) and is_list(value) ->
[i | _] = path
new_value = hide_at_not_hidden(Map.get(changeset.data, rel), i)
{Map.put(changeset.data, rel, new_value), new_value}
path == [] || match?([i] when is_integer(i), path) ->
{Map.update!(changeset.data, rel, &hide/1), nil}
true ->
{changeset.data, new_value}
end
end
else
{changeset.data, []}
end
changeset = mark_removed(changeset, new_value, (argument && argument.name) || rel.name)
{new_data, %{changeset | data: new_data}}
end
end
defp hide_at_not_hidden(values, i) do
values
|> Enum.reduce({0, []}, fn value, {counter, acc} ->
if hidden?(value) do
{counter, [value | acc]}
else
if counter == i do
{counter + 1, [hide(value) | acc]}
else
{counter + 1, [value | acc]}
end
end
end)
|> elem(1)
|> Enum.reverse()
end
@doc """
A utility to support "add" buttons on embedded types used in forms.
To use, simply pass in the form name of the embedded form as well as the name of the primary/outer form.
```elixir
# In your template, inside a form called `:change`
<button phx-click="append_thing" phx-value-path={{form.path}}>
</button>
# In the view/component
def handle_event("append_thing", %{"path" => path}, socket) do
changeset = add_embed(socket.assigns.changeset, path, "change")
{:noreply, assign(socket, changeset: changeset)}
end
```
You can also pass a specific value to be added, to seed the changes in a customized way.
By default, `%{}` is used.
"""
def add_embed(query, path, outer_form_name, add \\ %{})
2021-04-28 06:18:48 +12:00
def add_embed(%Ash.Changeset{} = changeset, original_path, outer_form_name, add) do
[^outer_form_name, key | path] = decode_path(original_path)
cond do
2021-04-28 06:18:48 +12:00
match?({x, y} when not is_nil(x) and not is_nil(y), argument_and_manages(changeset, key)) ->
add_related(changeset, original_path, outer_form_name, add: add)
attr = Ash.Resource.Info.attribute(changeset.resource, key) ->
current_value = Ash.Changeset.get_attribute(changeset, attr.name)
new_value = add_to_path(current_value, path, add)
new_value =
case attr.type do
{:array, _} -> List.wrap(new_value)
_ -> new_value
end
changeset
|> Ash.Changeset.change_attribute(attr.name, new_value)
|> mark_removed(new_value, attr.name)
arg = Enum.find(changeset.action.arguments, &(&1.name == key || to_string(&1.name) == key)) ->
current_value = Ash.Changeset.get_argument(changeset, arg.name)
new_value = add_to_path(current_value, path, add)
new_value =
case arg.type do
{:array, _} -> List.wrap(new_value)
_ -> new_value
end
changeset
|> Ash.Changeset.set_argument(arg.name, new_value)
|> mark_removed(new_value, arg.name)
true ->
changeset
end
end
def add_embed(%Ash.Query{} = query, path, outer_form_name, add) do
[^outer_form_name, key | path] = decode_path(path)
arg = Enum.find(query.action.arguments, &(&1.name == key || to_string(&1.name) == key))
if arg do
current_value = Ash.Query.get_argument(query, arg.name)
new_value = add_to_path(current_value, path, add)
new_value =
case arg.type do
{:array, _} -> List.wrap(new_value)
_ -> new_value
end
query
|> Ash.Changeset.set_argument(arg.name, new_value)
|> mark_removed(new_value, arg.name)
else
query
end
end
@doc """
A utility to support "remove" buttons on embedded types used in forms.
To use, simply pass in the form name of the embedded form as well as the name of the primary/outer form.
```elixir
# In your template, inside a form called `:change`
<button phx-click="remove_thing" phx-value-path={{form.path}}>
</button>
# In the view/component
def handle_event("remove_thing", %{"path" => path}, socket) do
changeset = remove_embed(socket.assigns.changeset, path, "change")
{:noreply, assign(socket, changeset: changeset)}
end
```
"""
def remove_embed(%Ash.Changeset{} = changeset, original_path, outer_form_name) do
[^outer_form_name, key | path] = decode_path(original_path)
cond do
match?({x, y} when not is_nil(x) and not is_nil(y), argument_and_manages(changeset, key)) ->
{_, changeset} = remove_related(changeset, original_path, outer_form_name)
changeset
attr = Ash.Resource.Info.attribute(changeset.resource, key) ->
current_value = Ash.Changeset.get_attribute(changeset, attr.name)
new_value =
if path == [] do
nil
else
new_value = remove_from_path(current_value, path)
2021-03-17 15:47:08 +13:00
case attr.type do
{:array, _} -> List.wrap(new_value)
_ -> new_value
end
end
changeset
|> Ash.Changeset.change_attribute(attr.name, new_value)
|> mark_removed(new_value, attr.name)
arg = Enum.find(changeset.action.arguments, &(&1.name == key || to_string(&1.name) == key)) ->
current_value = Ash.Changeset.get_argument(changeset, arg.name)
new_value =
if path == [] do
nil
else
new_value = remove_from_path(current_value, path)
2021-03-17 15:47:08 +13:00
case arg.type do
{:array, _} -> List.wrap(new_value)
_ -> new_value
end
end
changeset
|> Ash.Changeset.set_argument(arg.name, new_value)
|> mark_removed(new_value, arg.name)
true ->
changeset
end
end
def remove_embed(%Ash.Query{} = query, path, outer_form_name) do
[^outer_form_name, key | path] = decode_path(path)
arg = Enum.find(query.action.arguments, &(&1.name == key || to_string(&1.name) == key))
if arg do
current_value = Ash.Query.get_argument(query, arg.name)
new_value = remove_from_path(current_value, path)
new_value =
case arg.type do
{:array, _} -> List.wrap(new_value)
_ -> new_value
end
query
|> Ash.Changeset.set_argument(arg.name, new_value)
|> mark_removed(new_value, arg.name)
else
query
end
end
defp mark_removed(%Ash.Query{} = query, value, name) do
Ash.Query.put_context(query, :private, %{removed_keys: %{name => removed?(value)}})
end
defp mark_removed(%Ash.Changeset{} = changeset, value, name) do
Ash.Changeset.put_context(changeset, :private, %{
removed_keys: %{name => removed?(value)}
})
end
defp removed?(nil), do: true
defp removed?([]), do: true
defp removed?(other) do
other
|> List.wrap()
|> Enum.all?(&hidden?/1)
end
def add_to_path(nil, [], nil) do
[nil]
end
def add_to_path(nil, [], add) do
add
end
def add_to_path(value, [], nil) when is_list(value) do
value ++ [nil]
end
def add_to_path(value, [], add) when is_list(value) do
value ++ List.wrap(add)
end
def add_to_path(value, [], add) when is_map(value) do
2021-03-30 18:16:27 +13:00
case last_index(value) do
:error ->
%{"0" => value, "1" => add}
{:ok, index} ->
Map.put(value, index, add)
end
end
def add_to_path(value, [key | rest], add) when is_integer(key) and is_list(value) do
List.update_at(value, key, &add_to_path(&1, rest, add))
end
def add_to_path(empty, [key | rest], add) when is_integer(key) and empty in [nil, []] do
[add_to_path(nil, rest, add)]
end
def add_to_path(value, [0 | rest], add) when is_map(value) do
add_to_path(value, rest, add)
end
def add_to_path(value, [key | rest] = path, add) when is_integer(key) and is_map(value) do
case last_index(value) do
:error ->
add_to_path(%{"0" => value}, path, add)
_ ->
add_to_path(value, [to_string(key) | rest], add)
end
end
def add_to_path(value, [key | rest], add)
when (is_binary(key) or is_atom(key)) and is_map(value) do
cond do
Map.has_key?(value, key) ->
Map.update!(value, key, &add_to_path(&1, rest, add))
is_atom(key) && Map.has_key?(value, to_string(key)) ->
Map.update!(value, to_string(key), &add_to_path(&1, rest, add))
is_binary(key) && Enum.any?(Map.keys(value), &(to_string(&1) == key)) ->
Map.update!(value, String.to_existing_atom(key), &add_to_path(&1, rest, add))
true ->
Map.put(value, key, add_to_path(nil, rest, add))
end
end
def add_to_path(nil, [key | rest], add) when is_binary(key) or is_atom(key) do
%{key => add_to_path(nil, rest, add)}
end
def add_to_path([item], [key | _] = path, add) when is_binary(key) do
[add_to_path(item, path, add)]
end
def add_to_path(_, _, add), do: add
2021-03-30 18:16:27 +13:00
defp last_index(map) do
map
|> Map.keys()
|> Enum.map(&String.to_integer/1)
|> max_plus_one()
|> case do
nil ->
:error
value ->
{:ok, to_string(value)}
end
2021-03-30 18:16:27 +13:00
rescue
_ ->
:error
end
defp max_plus_one([]) do
nil
2021-03-30 18:16:27 +13:00
end
defp max_plus_one(list) do
list
|> Enum.max()
|> Kernel.+(1)
end
defp remove_from_path(value, [key]) when is_integer(key) and is_list(value) do
List.delete_at(value, key)
end
defp remove_from_path(value, [key]) when is_map(value) and (is_binary(key) or is_atom(key)) do
cond do
is_atom(key) ->
if is_struct(value) do
Map.put(value, key, nil)
else
Map.drop(value, [key, to_string(key)])
end
2021-03-30 18:35:15 +13:00
is_binary(key) && Enum.any?(Map.keys(value), &(is_atom(&1) && to_string(&1) == key)) ->
if is_struct(value) do
Map.put(value, String.to_existing_atom(key), nil)
else
Map.drop(value, [key, String.to_existing_atom(key)])
end
true ->
Map.delete(value, key)
end
2021-03-30 18:35:15 +13:00
|> case do
empty when empty == %{} ->
nil
value ->
value
end
end
defp remove_from_path(value, [key | rest]) when is_list(value) and is_integer(key) do
List.update_at(value, key, &remove_from_path(&1, rest))
end
2021-03-30 18:35:15 +13:00
defp remove_from_path(value, [key | rest]) when is_integer(key) and is_map(value) do
remove_from_path(value, [to_string(key) | rest])
end
defp remove_from_path(value, [key | rest])
when is_map(value) and (is_binary(key) or is_atom(key)) do
cond do
Map.has_key?(value, key) ->
Map.update!(value, key, &remove_from_path(&1, rest))
is_atom(key) && Map.has_key?(value, to_string(key)) ->
Map.update!(value, to_string(key), &remove_from_path(&1, rest))
2021-03-30 18:35:15 +13:00
is_binary(key) && Enum.any?(Map.keys(value), &(is_atom(&1) && to_string(&1) == key)) ->
Map.update!(value, String.to_existing_atom(key), &remove_from_path(&1, rest))
true ->
Map.put(value, key, remove_from_path(nil, rest))
end
end
defp remove_from_path([item], [key | _] = path) when is_binary(key) do
[remove_from_path(item, path)]
end
defp remove_from_path(value, _), do: value
@doc """
A utility for decoding the path of a form into a list.
For example:
2021-07-10 14:30:30 +12:00
```elixir
change[posts][0][comments][1]
["change", "posts", 0, "comments", 1]
```
"""
def decode_path(path) do
path = Plug.Conn.Query.decode(path)
do_decode_path(path)
end
defp do_decode_path(path) when is_map(path) and path != %{} do
path_part = Enum.at(path, 0)
rest = do_decode_path(elem(path_part, 1))
path_part
|> elem(0)
|> Integer.parse()
|> case do
{integer, ""} ->
[integer | rest]
_ ->
[elem(path_part, 0) | rest]
end
end
defp do_decode_path(""), do: []
defp do_decode_path(other) do
[other]
end
2020-10-21 06:54:35 +13:00
end