Skip to content

Commit

Permalink
Adds possibility to access Maps or Structs with get_field and uses St…
Browse files Browse the repository at this point in the history
…ring.to_existing_atom for safe key checking.
  • Loading branch information
dkln committed May 2, 2024
1 parent cdb0848 commit ade486e
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 39 deletions.
154 changes: 120 additions & 34 deletions lib/wuunder_utils/maps.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ defmodule WuunderUtils.Maps do
20
iex> WuunderUtils.Maps.get_field(%{value: 20}, "value")
nil
20
iex> WuunderUtils.Maps.get_field(%{value: 20}, "non-existent")
nil
Expand All @@ -38,27 +38,112 @@ defmodule WuunderUtils.Maps do
iex> WuunderUtils.Maps.get_field(%{value: 20}, "currency", "EUR")
"EUR"
iex> WuunderUtils.Maps.get_field(["a", "b", "c"], 1)
"b"
iex> WuunderUtils.Maps.get_field(["a", "b", "c"], 3, "d")
"d"
"""
@spec get_field(map(), map_key(), any()) :: any()
def get_field(params, key, default \\ nil)
@spec get_field(map() | list(), map_key() | non_neg_integer(), any()) :: any()
def get_field(map, key, default \\ nil)

def get_field(params, key, default)
when is_map(params) and is_valid_map_atom_key(key) do
if Map.has_key?(params, key) do
Map.get(params, key, default)
def get_field(map, index, default) when is_list(map) and is_number(index) do
Enum.at(map, index, default)
end

def get_field(map, key, default)
when is_map(map) and is_valid_map_atom_key(key) do
if Map.has_key?(map, key) do
Map.get(map, key, default)
else
Map.get(params, "#{key}", default)
Map.get(map, "#{key}", default)
end
end

def get_field(params, key, default) when is_map(params) and is_valid_map_binary_key(key) do
if Map.has_key?(params, key) do
Map.get(params, key, default)
else
default
def get_field(map, key, default) when is_map(map) and is_valid_map_binary_key(key) do
try do
atom_key = String.to_existing_atom(key)
Map.get(map, atom_key, default)
rescue
ArgumentError -> Map.get(map, key, default)
end
end

@doc """
Acts as Kernel.get_in but can also be used on Structs.
Has a lot of more extra functionalities:
- You can access lists (nested too)
- You can use mixed keys, they can be Atoms or Strings
- You can use a list to access the properties or a string representation
## Examples
iex> person = %Person{
...> country: %Country{code: "NL"},
...> address: %Address{
...> street: "Teststreet",
...> company: %Company{name: "Wuunder"}
...> },
...> meta: %{
...> skills: [
...> "programmer",
...> "manager",
...> %{type: "hobby", name: "painting"}
...> ]
...> }
...> }
...>
...> WuunderUtils.Maps.get_field_in(person, [:country, :code])
"NL"
iex> WuunderUtils.Maps.get_field_in(person, "country.code")
"NL"
iex> WuunderUtils.Maps.get_field_in(person, [:address, :company])
%Company{name: "Wuunder"}
iex> WuunderUtils.Maps.get_field_in(person, [:address, :company, :name])
"Wuunder"
iex> WuunderUtils.Maps.get_field_in(person, [:meta, :skills])
["programmer", "manager", %{name: "painting", type: "hobby"}]
iex> WuunderUtils.Maps.get_field_in(person, [:meta, :skills, 1])
"manager"
iex> WuunderUtils.Maps.get_field_in(person, "meta.skills.1")
"manager"
iex> WuunderUtils.Maps.get_field_in(person, [:meta, :skills, 2, :type])
"hobby"
iex> WuunderUtils.Maps.get_field_in(person, "meta.skills.2.type")
"hobby"
"""
@spec get_field_in(map() | struct() | nil, list(atom()) | String.t()) :: any()
def get_field_in(value, path) when is_binary(path) do
keys =
path
|> String.split(".")
|> Enum.map(fn key ->
if key =~ ~r/^[0-9]+$/ do
String.to_integer(key)
else
key
end
end)

get_field_in(value, keys)
end

def get_field_in(nil, _keys), do: nil

def get_field_in(value, []), do: value

def get_field_in(value, _keys) when not is_map(value) and not is_list(value), do: nil

def get_field_in(map_or_list, [key | rest]) when is_map(map_or_list) or is_list(map_or_list) do
map_or_list
|> get_field(key)
|> get_field_in(rest)
end

def get_field_in(nil, keys) when is_list(keys), do: nil

@doc """
Acts as an IndifferentMap. Put a key/value regardless of the key type. If the map
contains keys as atoms, the value will be stored as atom: value. If the map contains
Expand Down Expand Up @@ -208,12 +293,12 @@ defmodule WuunderUtils.Maps do
## Examples
iex> WuunderUtils.Maps.from_struct(%TestStruct{
iex> WuunderUtils.Maps.from_struct(%Person{
...> first_name: "Peter",
...> last_name: "Pan",
...> date_of_birth: ~D[1980-01-02],
...> weight: Decimal.new("81.5"),
...> country: %TestStruct2{code: "UK"},
...> country: %{code: "UK"},
...> time_of_death: ~T[13:37:37]
...> })
%{
Expand All @@ -223,19 +308,20 @@ defmodule WuunderUtils.Maps do
last_name: "Pan",
time_of_death: "13:37:37",
weight: "81.5",
country: %{code: "UK"}
country: %{code: "UK"},
meta: %{}
}
iex> WuunderUtils.Maps.from_struct(
...> %TestStruct{
...> %Person{
...> first_name: "Peter",
...> last_name: "Pan",
...> date_of_birth: ~D[1980-01-02],
...> weight: Decimal.new("81.5"),
...> country: %TestStruct2{code: "UK"},
...> country: %Country{code: "UK"},
...> time_of_death: ~T[13:37:37]
...> },
...> transform: [{TestStruct2, fn x -> "COUNTRY:" <> x.code end}]
...> transform: [{Country, fn x -> "COUNTRY:" <> x.code end}]
...> )
%{
address: nil,
Expand All @@ -244,12 +330,13 @@ defmodule WuunderUtils.Maps do
last_name: "Pan",
time_of_death: "13:37:37",
weight: "81.5",
country: "COUNTRY:UK"
country: "COUNTRY:UK",
meta: %{}
}
iex> WuunderUtils.Maps.from_struct(
...> %TestStruct{
...> address: %TestSchema{
...> %Person{
...> address: %Address{
...> street: "Straat",
...> number: 13,
...> zipcode: "1122AB"
Expand All @@ -258,18 +345,19 @@ defmodule WuunderUtils.Maps do
...> last_name: "Pan",
...> date_of_birth: ~D[1980-01-02],
...> weight: Decimal.new("81.5"),
...> country: %TestStruct2{code: "UK"},
...> country: %{code: "UK"},
...> time_of_death: ~T[13:37:37]
...> }
...> )
%{
address: %{number: 13, street: "Straat", zipcode: "1122AB"},
address: %{company: nil, number: 13, street: "Straat", zipcode: "1122AB"},
date_of_birth: "1980-01-02",
first_name: "Peter",
last_name: "Pan",
time_of_death: "13:37:37",
weight: "81.5",
country: %{code: "UK"}
country: %{code: "UK"},
meta: %{}
}
"""
Expand All @@ -281,14 +369,12 @@ defmodule WuunderUtils.Maps do
do: from_struct(value, default_struct_transforms() ++ extra_transformers)

def from_struct(%module{} = struct, transform) when is_list(transform) do
transform
|> Keyword.get(module)
|> case do
nil ->
transform_struct(module, struct, transform)

fun when is_function(fun, 1) ->
fun.(struct)
transform_fn = Keyword.get(transform, module)

if is_function(transform_fn, 1) do
transform_fn.(struct)
else
transform_struct(module, struct, transform)
end
end

Expand Down Expand Up @@ -383,7 +469,7 @@ defmodule WuunderUtils.Maps do
iex> WuunderUtils.Maps.present?(%{a: 1})
true
iex> WuunderUtils.Maps.present?(%TestStruct{})
iex> WuunderUtils.Maps.present?(%Person{})
true
iex> WuunderUtils.Maps.present?(%Ecto.Association.NotLoaded{})
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule WuunderUtils.MixProject do
def project do
[
app: :wuunder_utils,
version: "0.1.1",
version: "0.2.0",
elixir: "~> 1.14",
organization: "wuunder",
name: "Wuunder Utils",
Expand Down
19 changes: 15 additions & 4 deletions test/wuunder_utils/maps_test.exs
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
defmodule WuunderUtils.MapsTest do
use ExUnit.Case

defmodule TestStruct do
defmodule Person do
defstruct first_name: "",
last_name: "",
weight: nil,
date_of_birth: nil,
time_of_death: nil,
country: nil,
address: nil
address: nil,
meta: %{}
end

defmodule TestStruct2 do
defmodule Country do
defstruct code: ""
end

defmodule TestSchema do
defmodule Company do
use Ecto.Schema

@primary_key false
embedded_schema do
field(:name, :string)
end
end

defmodule Address do
use Ecto.Schema

@primary_key false
embedded_schema do
field(:street, :string)
field(:number, :integer)
field(:zipcode, :string)
embeds_one(:company, Company, on_replace: :delete)
end
end

Expand Down

0 comments on commit ade486e

Please sign in to comment.