We found ourselves copy-pasting a few useful "helper" functions
across our Elixir projects ...
it wasn't
"DRY",
so we created this library.
A library of useful functions
that we reach for
when building Elixir
Apps.
This library is used in our various Elixir
/ Phoenix
apps.
As with everything we do it's Open Source, Tested and Documented
so that anyone can benefit from it.
Install by adding useful
to your list of dependencies in mix.exs
:
def deps do
[
{:useful, "~> 1.14.0"}
]
end
Converts a Map
that has strings as keys (or mixed keys)
to have only atom keys. e.g:
# map that has different types of keys:
my_map = %{"name" => "Alex", id: 1}
Useful.atomize_map_keys(my_map)
%{name: Alex, id: 1}
Works recursively for deeply nested maps:
person = %{"name" => "Alex", id: 1, details: %{"age" => 17, height: 185}}
Useful.atomize_map_keys(person)
%{name: Alex, id: 1, details: %{age: 17, height: 185}}
Flatten a Map
of any depth/nesting:
iex> map = %{name: "alex", data: %{age: 17, height: 185}}
iex> Useful.flatten_map(map)
%{data__age: 17, data__height: 185, name: "alex"}
Note: flatten_map/1
converts all Map keys to Atom
as it's easier to work with atoms as keys
e.g: map.person__name
instead of map["person__name"]
.
We use the __
(double underscore)
as the delimiter for the keys of nested maps,
because if we attempt to use .
(period character)
we get an error:
iex(1)> :a.b
** (UndefinedFunctionError) function :a.b/0 is undefined (module :a is not available)
:a.b()
Get a deeply nested value from a map.
get_in_default/3
Proxies Kernel.get_in/2
but allows setting a default
value as the 3rd argument.
iex> map = %{name: "alex", detail: %{age: 17, height: 185}}
iex> Useful.get_in_default(map, [:data, :age])
17
iex> Useful.get_in_default(map, [:data, :everything], "Awesome")
"Awesome"
iex> Useful.get_in_default(conn, [:assigns, :person, :id], 0)
0
We needed this for getting conn.assigns.person.id
in our App
without having to write a bunch of boilerplate!
e.g:
person_id =
case Map.has_key?(conn.assigns, :person) do
false -> 0
true -> Map.get(conn.assigns.person, :id)
end
is just:
person_id = Useful.get_in_default(conn, [:assigns, :person, :id], 0)
Muuuuuuch cleaner/clearer! 😍
If any of the keys in the list is not found
it doesn't explode with errors,
simply returns the default
value 0
and continues!
Note: Code inspired by: stackoverflow.com/questions/48781427/optional-default-value-for-get-in
All credit to@PatNowak
🙌
The ideal syntax for this would be:
person_id = conn.assigns.person.id || 0
But Elixir
"Me no likey" ...
So this is what we have.
Turns a list of tuples with the same key into a list of tuples with unique keys. Useful when dealing with "multipart" forms that upload multiple files. e.g:
parts = [
{"files",[{"content-type", "image/png"},{"content-disposition","form-data; name=\"files\"; filename=\"first.png\""}],%Plug.Upload{path: "..", content_type: "image/png",filename: "first.png"}},
{"files",[{"content-type", "image/webp"},{"content-disposition","form-data; name=\"files\"; filename=\"second.webp\""}],%Plug.Upload{path: "...",content_type: "image/webp",filename: "second.webp"}}
]
Useful.list_tuples_to_unique_keys(parts) =
[
{"files-1",[{"content-type", "image/png"},{"content-disposition","form-data; name=\"files\"; filename=\"first.png\""}],%Plug.Upload{path: "..", content_type: "image/png",filename: "first.png"}},
{"files-2",[{"content-type", "image/webp"},{"content-disposition","form-data; name=\"files\"; filename=\"second.webp\""}],%Plug.Upload{path: "...",content_type: "image/webp",filename: "second.webp"}}
]
Remove an item
from a list
.
With numbers:
list = [1, 2, 3, 4]
Useful.remove_item_from_list(list, 3)
[1, 2, 4]
With a List
of Strings
:
list = ["climate", "change", "is", "not", "real"]
Useful.remove_item_from_list(list, "not")
["climate", "change", "is", "real"]
The list
is the first argument to the function
so it's easy to pipe:
get_list_of_items(person_id)
|> Useful.remove_item_from_list("item_to_be_removed")
|> etc.
Stringify a Map
e.g. to store it in a DB or log it stdout.
map = %{name: "alex", data: %{age: 17, height: 185}}
Useful.stringify_map(map)
"data__age: 17, data__height: 185, name: alex"
Stringify a tuple of any length; useful in debugging.
iex> tuple = {:ok, :example}
iex> Useful.stringify_tuple(tuple)
"ok: example"
truncate; To shorten (something) by, or as if by, cutting part of it off. wiktionary.org/wiki/truncate
Returns a truncated version of the String
according to the desired length
.
Useful if your displaying an uncertain amount of text in an interface.
E.g. the "bio" field on GitHub can be up 160 characters
.
Most people
don't have a bio
but some use every character.
If you're displaying profiles in an interface, you want a predictable length.
Usage:
iex> input = "You cannot lose what you never had."
iex> Useful.truncate(input, 18)
"You cannot lose ..."
The optional third argument terminator
allows specify any String
or an empty String
if you prefer
as the terminator for your truncated text:
iex> input = "do or do not there is no try"
iex> Useful.truncate(input, 12, "") # no ellipsis
"do or do not"
iex> input = "It was the best of times, it was the worst of times"
iex> Useful.trucate(input, 25, "")
"It was the best of times"
Returns the type of a variable, e.g: "function" or "integer"
Inspired by
typeof
from JavaScript
land.
iex> myvar = 42
iex> Useful.typeof(myvar)
"integer"
Empties the directory (deletes all files and any nested directories) recursively, but does not delete the actual directory. This is useful when you want to reset a directory, e.g. when testing.
iex> dir = "tmp" # contains lots of sub directories and files
iex> Useful.empty_dir_contents(dir)
{:ok, dir}
Detailed docs available at: https://hexdocs.pm/useful/Useful.html
If you need a specific helper function or utility (e.g: something you found useful in a different programming language), please open an issue so that we can all benefit from useful functions.
Thanks!