Elixir Compile-Time Validation
Compile-time validation is checking a value while code compiles, so a bad value stops the build. A macro does the check because it sees the literal value before any code runs.
A macro can allow a good value and reject a bad one
The good port prints. The bad port is compiled from text, and its fixed message prints instead of stopping the script.
defmodule Validator do
defmacro port(number) do
if not is_integer(number) or number < 1 or number > 65535 do
raise ArgumentError, message: "port out of range"
end
quote do
unquote(number)
end
end
end
defmodule Caller do
require Validator
def value, do: Validator.port(8080)
end
IO.puts(Caller.value())
code = "defmodule Bad do\n require Validator\n def value, do: Validator.port(70000)\nend"
try do
Code.compile_string(code)
IO.puts("compiled")
rescue
e -> IO.puts(Exception.message(e))
endA macro can allow only named values
Green sits in the allowed list, so the caller prints it. A name outside the list would raise during compilation.
defmodule Checker do
defmacro allow(name) do
if name not in [:red, :green] do
raise ArgumentError, message: "colour not allowed"
end
quote do
unquote(name)
end
end
end
defmodule Caller do
require Checker
def colour, do: Checker.allow(:green)
end
IO.inspect(Caller.colour())A macro can check a route and keep its shape
The route method and path pass their checks. Escaping keeps the three-part route whole when it is inserted.
defmodule RouteCheck do
defmacro route(method, path, handler) do
if method not in [:get, :post] do
raise ArgumentError, message: "unknown method"
end
if not is_binary(path) or not String.starts_with?(path, "/") do
raise ArgumentError, message: "bad path"
end
tuple = {method, path, handler}
quote do
unquote(Macro.escape(tuple))
end
end
end
defmodule Caller do
require RouteCheck
def home, do: RouteCheck.route(:get, "/home", :home)
end
IO.inspect(Caller.home())Example
defmodule RouteCheck do
defmacro route(method, path, handler) do
if method not in [:get, :post] do
raise ArgumentError, message: "unknown method"
end
if not is_binary(path) or not String.starts_with?(path, "/") do
raise ArgumentError, message: "bad path"
end
tuple = {method, path, handler}
quote do
unquote(Macro.escape(tuple))
end
end
end
defmodule Caller do
require RouteCheck
def home, do: RouteCheck.route(:get, "/home", :home)
def save, do: RouteCheck.route(:post, "/save", :save)
end
IO.inspect(Caller.home())
IO.inspect(Caller.save())
code = "defmodule BadRoute do\n require RouteCheck\n def bad, do: RouteCheck.route(:delete, \"/gone\", :gone)\nend"
try do
Code.compile_string(code)
IO.puts("compiled")
rescue
e -> IO.puts(Exception.message(e))
endKey points
- Literal: a literal is a value written directly in code, such as 8080 or :green.
- Check in macro: a macro can test a literal value and raise an error when the value breaks a rule.
- Raise: raising stops compilation with a fixed message that names the broken rule.
- Route: a route is a three-part value holding a method, a path, and a handler name.
- Escape: Macro.escape turns a three-part route into a safe tree form before it is inserted.
In this example, two good routes print in full, and a bad route compiled from text prints its fixed message instead of stopping the script.
Note
only literal values can be checked this way, because a macro cannot see a value that is computed at run time.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
What happens when a macro check fails on a literal value?