Elegantly Running multiple validators in Elixir

byronsalty

Byron Salty

Posted on January 7, 2024

Elegantly Running multiple validators in Elixir

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
  ...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And all the tests pass:

Finished in 0.08 seconds (0.00s async, 0.08s sync)
3 tests, 0 failures
Enter fullscreen mode Exit fullscreen mode

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.

Follow on MediumDev.toTwitter, or LinkedIn.

💖 💪 🙅 🚩
byronsalty
Byron Salty

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