Last active
September 25, 2019 16:11
-
-
Save DoggettCK/743876e1d303abc7ab52c6852db2ba2b to your computer and use it in GitHub Desktop.
Password generation with macros in Elixir
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 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