Skip to content

Latest commit

 

History

History
298 lines (221 loc) · 8.13 KB

README.md

File metadata and controls

298 lines (221 loc) · 8.13 KB

useful

A collection of useful functions for building Elixir Apps.

GitHub Workflow Status codecov.io Hex.pm Libraries.io dependency status contributions welcome HitCount

swiss-army-knife

Why? 🤷

We found ourselves copy-pasting a few useful "helper" functions across our Elixir projects ...
it wasn't "DRY", so we created this library.

What? 💭

A library of useful functions that we reach for when building Elixir Apps.

Who? 👤

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.

How? 💻

Install ⬇️

Install by adding useful to your list of dependencies in mix.exs:

def deps do
  [
    {:useful, "~> 1.14.0"}
  ]
end

Function Reference

atomize_map_keys/1

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_map/1

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_in_default/1

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.

list_tuple_to_unique_keys/1

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_item_from_list/2

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_map/1

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_tuple/1

Stringify a tuple of any length; useful in debugging.

iex> tuple = {:ok, :example}
iex> Useful.stringify_tuple(tuple)
"ok: example"

truncate/3

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"

typeof/1

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"

empty_dir_contents/1

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}

Docs 📜

Detailed docs available at: https://hexdocs.pm/useful/Useful.html


Help Us Help You! 🙏

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!