Elegantly Running multiple validators in Elixir
Published: 2024-01-07 23:33:32

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](https://github.com/fireproofsocks/spelling_bee/blob/main/lib/spelling_bee.ex)

And here is [another article](https://dev.to/martinthenth/using-elixirs-with-statement-5e36) 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](https://byronsalty.medium.com/list/leadership-37d7992c55ba) and [AI](https://byronsalty.medium.com/list/my-ai-dc4873d307bf).

Development articles [here](https://dev.to/byronsalty).

Follow on [Medium](https://byronsalty.medium.com/), [Dev.to](https://dev.to/byronsalty), [Twitter](https://twitter.com/byronsalty), or [LinkedIn](https://www.linkedin.com/in/byronsalty/).