Implement a Validator module using data structure

In official DSLs document: Domain-Specific Languages (DSLs) — Elixir v1.18.4 , and in the very first example, it says we could implement a validator using data structures, and it provides the usage code here:

# 1. Data structures
import Validator
validate user, name: [length: 1..100], email: [matches: ~r/@/]

I used functions and macros before, so I understand the #2 and #3 examples. But English is not my native language, and I’m new to the DSLs thing, so I am wondering what is the module definition for the Validator in this example so that we can use it in the line:
validate user, name: [length: 1..100], email: [matches: ~r/@/]

I can’t find any answer from this forum, Google search, etc, so it seems to be very obvious for everyone. It makes me feel frustrated, but I really wanna know the answer. Please help to provide the example code for the definition of the ‘validate’, thanks in advance.

defmodule Validator do
# What is the definition of the 'validate' here?
# It can't be a 'def validate, do: :something', right? That's a function definition.
end

The purpose of the introduction to that guide is to compare and contrast between data, functions, and macros. All of the examples are approaches you could take to do the same thing.

The validate(...) function would be a function defined with def. The difference between the first and second examples is that the first is composed using data (passing in more keys) while the second is composed using functions.

So in the first example, if you wanted to add another validation, you would add another key/value pair to the opts list, like:

validate(user, length: 1..100, ascii_only: true)

Whereas in the second example, you would instead add another function, like:

user
|> validate_length(1..100)
|> validate_ascii_only()

A simple implementation of the first example might look something like this, where we essentially transform the first example into the second.

def validate(user, opts) do
  user
  |> maybe_validate_length(opts[:length])
  # ...
end

defp maybe_validate_length(user, nil), do: user
defp maybe_validate_length(user, range), do: validate_length(user, range)

Or, perhaps more in the spirit of the example, it could be something like this:

@validators [
  length: {Validators, :validate_length},
  ascii_only: {Validators, :validate_ascii_only},
]

def validate(user, opts) do
  Enum.reduce(opts, user, fn {k, v}, acc ->
    {mod, fun} = Map.fetch!(@validators, k)
    apply(mod, fun, [acc, v])
  end)
end
1 Like

Wow, very great answer! It helps me a lot, thank you very much! :clap: :clap: :clap: :clap: :clap: