Elegantly Running multiple validators in Elixir
Byron Salty
Posted on January 7, 2024
I need to update some validation code to be elegant and not this chained set of if
statements.
def validate_guess(prompt, words) do
if not Enum.all?(words, fn w -> String.trim(w) != "" end) do
"Please fill in all the blanks"
else
if Enum.any?(words, fn w -> String.trim(w) |> String.length() < 3 end) do
"No words less than 3 letters"
else
if Enum.any?(words, fn w -> Regex.match?(~r/\W/, w) end) do
"Words can only contain letters"
else
...
How can we write this better?
Step 1 - write some tests
Despite how the code looks, it works. Let's make sure we have tests in place before we refactor so that it continues to work.
Writing tests will also help refactor the code, because we need to make the interfaces easily testable.
describe "test validations" do
test "test for blank words" do
prompt = nil
words = ["", "dog", "walking", "around", "tree"]
assert TextHelper.validate_guess(prompt, words) == "Please fill in all the blanks"
end
test "test for only letters" do
prompt = nil
words = ["some words", "dog", "walking", "around", "tree"]
assert TextHelper.validate_guess(prompt, words) == "Words can only contain letters"
words = ["brown1123", "dog", "walking", "around", "tree"]
assert TextHelper.validate_guess(prompt, words) == "Words can only contain letters"
words = ["brown!", "dog", "walking", "around", "tree"]
assert TextHelper.validate_guess(prompt, words) == "Words can only contain letters"
end
test "test for no words less than 3 letters" do
prompt = nil
words = ["br", "dog", "walking", "around", "tree"]
assert TextHelper.validate_guess(prompt, words) == "No words less than 3 letters"
end
end
Note: I did end up finding an error where my former code was not detecting (rejecting) words with digits, so a win for unit tests!
Step 2 - Rewrite using with
statement
I saw this structure in this project:
SpellingBee project
And here is another article talking specifically about using the with
statement for this reason.
A much cleaner way to combine all of the validations:
def validate_guess(prompt, words) do
with "" <- validate_blank_words(words),
"" <- validate_word_characters(words),
"" <- validate_word_length(words),
"" <- validate_grammar(prompt, words) do
""
end
end
Individual validators:
defp validate_blank_words(words) do
case Enum.any?(words, fn w -> String.trim(w) == "" end) do
true -> "Please fill in all the blanks"
false -> ""
end
end
defp validate_word_length(words) do
case Enum.any?(words, fn w -> String.trim(w) |> String.length() < 3 end) do
true -> "No words less than 3 letters"
false -> ""
end
end
defp validate_word_characters(words) do
case Enum.any?(words, fn w -> Regex.match?(~r/[\W\d]/, w) end) do
true -> "Words can only contain letters"
false -> ""
end
end
And all the tests pass:
Finished in 0.08 seconds (0.00s async, 0.08s sync)
3 tests, 0 failures
Woot!
If you found this article helpful, show your support with a Like, Comment, or Follow.
Read more of Byron’s articles about Leadership and AI.
Development articles here.
Posted on January 7, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024