Created
January 29, 2024 14:03
-
-
Save mayel/16eddb9b87615072207e897d53edf816 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
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