Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Password generation with macros in Elixir
defmodule ChooseFrom do
defmacro __using__(options) do
Enum.map(options, fn {name, alphabet} ->
function_name = :"choose_#{name}"
chars = String.graphemes(alphabet)
build_alphabet(name, function_name, chars)
end) ++ [
quote do
defp generate_minimums(chars, _options), do: chars
end
]
end
defp build_alphabet(name, function_name, chars) do
quote do
defp unquote(function_name)(remaining) when is_integer(remaining) and remaining > 0 do
unquote(function_name)([], remaining)
end
defp unquote(function_name)(_), do: []
defp unquote(function_name)(chosen, remaining)
when is_integer(remaining) and remaining > 0 do
next_char = Enum.random(unquote(chars))
unquote(function_name)([next_char | chosen], remaining - 1)
end
defp unquote(function_name)(chosen, _), do: chosen
defp generate_minimums(chars, %{unquote(name) => minimum} = options) do
chars
|> unquote(function_name)(minimum)
|> generate_minimums(Map.delete(options, unquote(name)))
end
end
end
end
defmodule PasswordGenerator do
use ChooseFrom,
lower: "abcdefghijklmnopqrstuvwxyz",
upper: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
digits: "0123456789",
special: "~`!@#$%^&*?"
@alphabets ~w(lower upper digits special)a
def generate_password(min_length, options \\ []) do
{allowed_sets, options} = Keyword.pop(options, :allowed)
allowed_charsets =
allowed_sets
|> whitelist_allow(@alphabets)
whitelisted_options =
options
|> Keyword.take(allowed_charsets)
|> Enum.into(%{})
chars =
generate_minimums([], whitelisted_options)
remaining = min_length - length(chars)
chars
|> generate_remaining(remaining, allowed_charsets)
|> List.flatten()
|> Enum.shuffle()
|> Enum.join()
end
defp whitelist_allow(nil, whitelist), do: whitelist
defp whitelist_allow([], whitelist), do: whitelist
defp whitelist_allow(candidates, whitelist) when is_list(candidates) do
candidate_set = MapSet.new(candidates)
whitelist_set = MapSet.new(whitelist)
candidate_set
|> MapSet.intersection(whitelist_set)
|> Enum.to_list()
end
defp whitelist_allow(_, whitelist), do: whitelist
defp generate_remaining(chars, remaining, allowed_charsets) when remaining > 0 do
func =
allowed_charsets
|> Enum.random()
|> choose_function()
chars
|> func.(1)
|> generate_remaining(remaining - 1, allowed_charsets)
end
defp generate_remaining(chars, 0, _allowed_charsets), do: chars
defp choose_function(:lower), do: &choose_lower/2
defp choose_function(:upper), do: &choose_upper/2
defp choose_function(:digits), do: &choose_digits/2
defp choose_function(:special), do: &choose_special/2
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment