RTypes is an Elixir library which helps automatically create a validation function for a given user type. The function can be used to check the shape of the data after de-serialisation or in unit-tests.
Let's suppose we have a type
@type t :: 0..255
and we have a value x
. To ensure that our value corresponds to the type t
we
can use the function
def is_t(x) when is_integer(x) and x >= 0 and x <= 255
Now, if we have a compound type
@type list_of_ts :: [t]
and a value xs
, we can use is_list/1
guard on xs
and then ensure that all
elements of the list conform to t
. And if we have a more complex structure
@type state(a, b) :: %{key1: {a, b}, key2: list_of_ts()}
and a value s
, we can check that s
is a map which has keys key1
and
key2
, apply the logic above for the value of key2
and for any concrete types
a
and b
we can check that the value of key1
is a tuple of length 2 and its
elements conform to a
and b
respectively. So we just recursively apply those
checks.
That's the gist of it.
The library defines make_validator/1
and make_predicate/1
macros, and
make_validator/3
and make_predicate/3
functions which can be used to
build the functions at run time. The difference between the two is that a
validator
returns :ok
or {:error, reason}
where reason
explains what went
wrong, while a predicate
returns only true
or false
.
iex> require RTypes
iex> port_number? = RTypes.make_predicate(:inet.port_number())
iex> port_number?.(8080)
true
iex> port_number?.(80000)
false
iex> validate_is_kwlist = RTypes.make_validator(Keyword, :t, [{:type, 0, :pos_integer, []}])
iex> validate_is_kwlist.(key1: 4, key2: 5)
:ok
iex> {:error, _reason} = validate_is_kwlist.([1, 2, 3])
The library provides Generator
module which defines the make/4
function and make/2
macro to be used with property-based
frameworks. The
StreamData
backend
is provided with the library, while
PropCheck
backend can be
found in rtypes_propcheck
library.
For example, to write a unit test for a pure function with a given
spec to use with StreamData
framework one could write something
along the lines:
defmodule MyTest do
use ExUnit.Case
use ExUnitProperties
require RTypes
require RTypes.Generator, as: Generator
# @spec f(arg_type) :: result_type
arg_type_gen = Generator.make(arg_type, Generator.StreamData)
result_type? = RTypes.make_predicate(result_type)
property \"for any parameter `f/1` returs value of `result_type`\" do
check all value <- arg_type_gen do
assert result_type?.(f(value))
end
end
end
A generated validation function is essentially a walk-the-tree interpreter of the expanded AST that represents the type. However, instead of evaluating the AST it applies basic type checks.
A generated predicate uses a different approach. It builds up a chain of
suspended function calls (closures) which mirrors the type's AST. The benchmarks
in bench/
directory have shown that it works approximately 2x faster than the
interpreted version. The downside is that it provides no explanation or failed
cases.
-
The type must be fully instantiated, that is, all the type parameters should be of a concrete type.
-
For practical reasons the generated function does not recurse down to
iolist()
, making only some simplified tests.
-
Handle recursive types.
-
Data generatorDONE. -
Better error messages.