Skip to content

Instantly share code, notes, and snippets.

@mayel
Created January 29, 2024 14:03
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 mayel/16eddb9b87615072207e897d53edf816 to your computer and use it in GitHub Desktop.
Save mayel/16eddb9b87615072207e897d53edf816 to your computer and use it in GitHub Desktop.
defmodule UserInput do
@doc "Takes a data structure and converts any keys in maps to (previously defined) atoms, recursively. By default any unknown string keys will be discarded. It can optionally also convert string values to known atoms as well."
def input_to_atoms(
data,
opts \\ []
)
def input_to_atoms(data, opts) do
opts =
opts
|> Keyword.put_new(:discard_unknown_keys, true)
|> Keyword.put_new(:values, false)
|> Keyword.put_new(:also_discard_unknown_nested_keys, true)
# |> Keyword.put_new(:nested_discard_unknown, false)
|> Keyword.put_new(:to_snake, false)
|> Keyword.put_new(:values_to_integers, false)
input_to_atoms(
data,
opts[:discard_unknown_keys],
opts[:values],
opts[:also_discard_unknown_nested_keys],
false,
opts[:to_snake],
opts[:values_to_integers]
)
end
defp input_to_atoms(
enum,
discard_unknown_keys,
including_values,
also_discard_unknown_nested_keys,
force,
to_snake,
values_to_integers
)
defp input_to_atoms(data, _, _, _, _, _, _) when is_struct(data) do
# skip structs
data
end
defp input_to_atoms(
%{} = data,
true = discard_unknown_keys,
including_values,
also_discard_unknown_nested_keys,
force,
to_snake,
values_to_integers
) do
# turn any keys into atoms (if such atoms already exist) and discard the rest
:maps.filter(
fn k, _v -> is_atom(k) end,
data
|> Map.drop(["_csrf_token", "_persistent_id"])
|> Map.new(fn {k, v} ->
{
maybe_to_atom_or_module(k, force, to_snake),
if(also_discard_unknown_nested_keys,
do:
input_to_atoms(
v,
true,
including_values,
true,
force,
to_snake,
values_to_integers
),
else:
input_to_atoms(
v,
false,
including_values,
false,
force,
to_snake,
values_to_integers
)
)
}
end)
)
end
defp input_to_atoms(
%{} = data,
false = discard_unknown_keys,
including_values,
also_discard_unknown_nested_keys,
force,
to_snake,
values_to_integers
) do
data
|> Map.drop(["_csrf_token"])
|> Map.new(fn {k, v} ->
{
maybe_to_atom_or_module(k, force, to_snake) || k,
input_to_atoms(
v,
false,
including_values,
also_discard_unknown_nested_keys,
force,
to_snake,
values_to_integers
)
}
end)
end
defp input_to_atoms(
list,
true = discard_unknown_keys,
including_values,
also_discard_unknown_nested_keys,
force,
to_snake,
values_to_integers
)
when is_list(list) do
if Keyword.keyword?(list) and list != [] do
Map.new(list)
|> input_to_atoms(
true,
including_values,
also_discard_unknown_nested_keys,
force,
to_snake,
values_to_integers
)
else
Enum.map(
list,
&input_to_atoms(
&1,
false,
including_values,
also_discard_unknown_nested_keys,
force,
to_snake,
values_to_integers
)
)
end
end
defp input_to_atoms(
list,
_false = discard_unknown_keys,
including_values,
also_discard_unknown_nested_keys,
force,
to_snake,
values_to_integers
)
when is_list(list) do
if Keyword.keyword?(list) and list != [] do
Map.new(list)
|> input_to_atoms(
false,
including_values,
also_discard_unknown_nested_keys,
force,
to_snake,
values_to_integers
)
else
Enum.map(
list,
&input_to_atoms(
&1,
false,
including_values,
also_discard_unknown_nested_keys,
force,
to_snake,
values_to_integers
)
)
end
end
# defp input_to_atoms(
# {key, val},
# discard_unknown_keys,
# including_values,
# also_discard_unknown_nested_keys,
# force,
# to_snake,
# values_to_integers
# )
defp input_to_atoms(
other,
discard_unknown_keys,
including_values,
also_discard_unknown_nested_keys,
force,
to_snake,
values_to_integers
),
do:
input_to_value(
other,
discard_unknown_keys,
including_values,
also_discard_unknown_nested_keys,
force,
to_snake,
values_to_integers
)
# support truthy/falsy values
def input_to_value("nil", _, true = _including_values, _, _, _, _), do: nil
def input_to_value("false", _, true = _including_values, _, _, _, _), do: false
def input_to_value("true", _, true = _including_values, _, _, _, _), do: true
def input_to_value(
v,
_,
true = _including_values,
_,
force,
_to_snake,
true = _values_to_integers
)
when is_binary(v) do
maybe_to_integer(v, nil) || maybe_to_module(v, force) || maybe_to_atom(v) ||
v
end
def input_to_value(v, _, true = _including_values, _, force, _to_snake, _values_to_integers)
when is_binary(v) do
maybe_to_module(v, force) || maybe_to_atom(v) || v
end
def input_to_value(v, _, _, _, _, _, true = _values_to_integers) when is_binary(v),
do: maybe_to_integer(v, nil) || v
def input_to_value(v, _, _, _, _, _, _), do: v
@doc "Takes a string and returns an atom if it can be converted to one, else returns the input itself"
def maybe_to_atom(str) when is_binary(str) do
maybe_to_atom!(str) || str
end
def maybe_to_atom(other), do: other
@doc "Takes a string or an atom and returns an atom if it is one or can be converted to one, else returns nil."
def maybe_to_atom!("false"), do: false
def maybe_to_atom!("nil"), do: nil
def maybe_to_atom!(""), do: nil
def maybe_to_atom!(str) when is_binary(str) do
try do
String.to_existing_atom(str)
rescue
ArgumentError -> nil
end
end
def maybe_to_atom!(atom) when is_atom(atom), do: atom
def maybe_to_atom!(_), do: nil
@doc "Takes a string and returns the corresponding Elixir module if it exists and is not disabled in the app."
def maybe_to_module(str, force \\ true)
def maybe_to_module("Elixir." <> _ = str, force) do
case maybe_to_atom(str) do
module_or_atom when is_atom(module_or_atom) and not is_nil(module_or_atom) ->
maybe_to_module(module_or_atom, force)
_ ->
nil
end
end
def maybe_to_module(str, force) when is_binary(str) do
maybe_to_module("Elixir." <> str, force)
end
def maybe_to_module(atom, force) when is_atom(atom) and not is_nil(atom) do
if force != true or module_enabled?(atom) do
atom
else
nil
end
end
def maybe_to_module(_, _), do: nil
def maybe_to_atom_or_module(k, _force, _to_snake) when is_atom(k),
do: k
# def maybe_to_atom_or_module(k, true = force, true = _to_snake),
# do: maybe_to_module(k, force) || Text.maybe_to_snake(k) |> String.to_atom()
# def maybe_to_atom_or_module(k, _false = force, true = _to_snake),
# do: maybe_to_module(k, force) || maybe_to_snake_atom(k)
def maybe_to_atom_or_module(k, true = force, _false = _to_snake) when is_binary(k),
do: maybe_to_module(k, force) || String.to_atom(k)
def maybe_to_atom_or_module(k, _false = force, _false = _to_snake),
do: maybe_to_module(k, force) || maybe_to_atom(k)
@doc "Converts a value to a floating-point number if possible. If the value cannot be converted to a float, it returns a fallback value (which defaults to 0 if not provided)"
def maybe_to_float(val, fallback \\ 0)
def maybe_to_float(num, _fallback) when is_integer(num) or is_float(num), do: num
def maybe_to_float(str, fallback) do
case Float.parse(str) do
{num, ""} ->
num
{num, extra} ->
warn(extra, "Do not convert value because Float.parse found extra data in the input")
fallback
_ ->
fallback
end
end
@doc "Converts a value to an integer if possible. If the value is not an integer, it attempts to convert it to a float and then rounds it to the nearest integer. Otherwise it returns a fallback value (which defaults to 0 if not provided)."
def maybe_to_integer(val, fallback \\ 0)
def maybe_to_integer(val, _fallback) when is_integer(val), do: val
def maybe_to_integer(val, _fallback) when is_float(val), do: round(val)
def maybe_to_integer(val, fallback) do
case maybe_to_float(val, nil) do
nil ->
fallback
float ->
round(float)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment