Current section
Files
Jump to
Current section
Files
lib/property_generator/validators.ex
defmodule PropertyGenerator.Validators do
@moduledoc """
Functions for creating type validators from type specifications.
"""
@doc """
Creates an output validator from a type specification.
"""
@spec create_output_validator(any()) :: (any() -> boolean())
def create_output_validator(output_type) do
type_to_validator(output_type)
end
@doc """
Converts a type specification to a validator function.
"""
@spec type_to_validator(any()) :: (any() -> boolean())
def type_to_validator({:type, _, :integer, []}), do: &is_integer/1
def type_to_validator({:type, _, :float, []}), do: &is_float/1
def type_to_validator({:type, _, :number, []}), do: &is_number/1
def type_to_validator({:type, _, :boolean, []}), do: &is_boolean/1
def type_to_validator({:type, _, :binary, []}), do: &is_binary/1
def type_to_validator({:type, _, :bitstring, []}), do: &is_bitstring/1
def type_to_validator({:type, _, :atom, []}), do: &is_atom/1
def type_to_validator({:type, _, :string, []}), do: &is_binary/1
def type_to_validator({:type, _, :list, []}), do: &is_list/1
def type_to_validator({:type, _, :map, []}), do: &is_map/1
def type_to_validator({:type, _, nil, []}), do: &is_nil/1
def type_to_validator({:type, _, :any, []}), do: fn _ -> true end
def type_to_validator({:type, _, :term, []}), do: fn _ -> true end
def type_to_validator({:type, _, :no_return, []}), do: fn _ -> false end
def type_to_validator({:type, _, :iodata, []}) do
fn value -> is_binary(value) or is_list(value) end
end
def type_to_validator({:atom, _, atom_value}), do: fn value -> value == atom_value end
def type_to_validator({:integer, _, int_value}), do: fn value -> value == int_value end
def type_to_validator({:type, _, :keyword, []}) do
fn value ->
is_list(value) and
Enum.all?(value, fn
{key, _val} when is_atom(key) -> true
_ -> false
end)
end
end
def type_to_validator({:type, _, :keyword, [value_type]}) do
value_validator = type_to_validator(value_type)
fn value ->
is_list(value) and
Enum.all?(value, fn
{key, val} when is_atom(key) -> value_validator.(val)
_ -> false
end)
end
end
def type_to_validator({:type, _, :charlist, []}) do
fn value -> is_list(value) and Enum.all?(value, &is_integer/1) end
end
def type_to_validator({:type, _, :non_neg_integer, []}) do
fn value -> is_integer(value) and value >= 0 end
end
def type_to_validator({:type, _, :pos_integer, []}) do
fn value -> is_integer(value) and value > 0 end
end
def type_to_validator({:type, _, :neg_integer, []}) do
fn value -> is_integer(value) and value < 0 end
end
def type_to_validator({:type, _, :range, [min, max]}) do
min_val = extract_integer_value(min, 0)
max_val = extract_integer_value(max, 100)
fn value -> is_integer(value) and value >= min_val and value <= max_val end
end
def type_to_validator({:type, _, :list, [element_type]}) do
case element_type do
{:type, _, :tuple, [{:atom, _, key}, value_type]} ->
# Keyword list validator
value_validator = type_to_validator(value_type)
fn list ->
is_list(list) and
Enum.any?(list, fn
{^key, value} -> value_validator.(value)
_ -> false
end)
end
_ ->
# Regular list validator
element_validator = type_to_validator(element_type)
fn list -> is_list(list) and Enum.all?(list, element_validator) end
end
end
def type_to_validator({:type, _, :tuple, element_types}) do
element_validators = Enum.map(element_types, &type_to_validator/1)
expected_size = length(element_types)
fn tuple ->
is_tuple(tuple) and tuple_size(tuple) == expected_size and
validate_tuple_elements(tuple, element_validators)
end
end
def type_to_validator({:type, _, :map, field_types}) when is_list(field_types) do
struct_field = find_struct_field(field_types)
case struct_field do
{:type, _, :map_field_exact, [{:atom, _, :__struct__}, {:atom, _, module_name}]} ->
validate_struct(module_name, field_types)
nil ->
validate_map(field_types)
end
end
def type_to_validator({:type, _, :fun, [_args, _return]}) do
# Function type - validate it's a function
&is_function/1
end
def type_to_validator({:type, _, :union, types}) do
validators = Enum.map(types, &type_to_validator/1)
fn value -> Enum.any?(validators, fn validator -> validator.(value) end) end
end
def type_to_validator({:remote_type, _, [{:atom, _, String}, {:atom, _, :t}, []]}) do
&is_binary/1
end
def type_to_validator(_type), do: fn _ -> true end
# Private helper functions
@spec extract_integer_value(any(), integer()) :: integer()
defp extract_integer_value({:integer, _, val}, _default), do: val
@spec extract_integer_value(any(), integer()) :: integer()
defp extract_integer_value(val, _default) when is_integer(val), do: val
@spec extract_integer_value(any(), integer()) :: integer()
defp extract_integer_value(_val, default), do: default
@spec find_struct_field([any()]) :: any() | nil
defp find_struct_field(field_types) do
Enum.find(field_types, fn
{:type, _, :map_field_exact, [{:atom, _, :__struct__}, {:atom, _, _module}]} -> true
_ -> false
end)
end
@spec validate_tuple_elements(tuple(), [(any() -> boolean())]) :: boolean()
defp validate_tuple_elements(tuple, validators) do
validators
|> Enum.with_index()
|> Enum.all?(fn {validator, index} -> validator.(elem(tuple, index)) end)
end
@spec validate_struct(atom(), [any()]) :: (any() -> boolean())
defp validate_struct(module_name, field_types) do
other_fields =
Enum.reject(field_types, fn
{:type, _, :map_field_exact, [{:atom, _, :__struct__}, _]} -> true
_ -> false
end)
required_validators = Enum.flat_map(other_fields, &create_field_validator/1)
fn value ->
is_struct(value, module_name) and
Enum.all?(required_validators, fn {field_name, validator} ->
field_value = Map.get(value, field_name)
validator.(field_value)
end)
end
end
@spec create_field_validator(any()) :: [{atom(), (any() -> boolean())}]
defp create_field_validator({:type, _, :map_field_exact, [{:atom, _, field_name}, value_type]}) do
# Required field
value_validator = type_to_validator(value_type)
[{field_name, value_validator}]
end
@spec create_field_validator(any()) :: [{atom(), (any() -> boolean())}]
defp create_field_validator({:type, _, :map_field_assoc, [_key_type, _value_type]}) do
# Optional field - don't validate
[]
end
@spec validate_map([any()]) :: (any() -> boolean())
defp validate_map(field_types) do
# Separate required and optional fields
required_fields =
field_types
|> Enum.filter(fn
{:type, _, :map_field_exact, _} -> true
_ -> false
end)
|> Enum.map(fn {:type, _, :map_field_exact, [key_type, value_type]} ->
{type_to_validator(key_type), type_to_validator(value_type)}
end)
fn map ->
is_map(map) and
Enum.all?(required_fields, fn {key_validator, value_validator} ->
# Check that at least one key-value pair matches this required field
Enum.any?(map, fn {key, value} ->
key_validator.(key) and value_validator.(value)
end)
end)
end
end
end