Current section

Files

Jump to
oban lib oban validation.ex
Raw

lib/oban/validation.ex

defmodule Oban.Validation do
@moduledoc false
alias Oban.Cron.Expression
@type validator ::
({atom(), term()} ->
:ok
| {:error, term()})
def validate(parent_key \\ nil, opts, validator)
def validate(_parent_key, opts, validator) when is_list(opts) do
Enum.reduce_while(opts, :ok, fn opt, acc ->
case validator.(opt) do
nil -> {:cont, acc}
:ok -> {:cont, acc}
{:error, _reason} = error -> {:halt, error}
end
end)
end
def validate(parent_key, opts, _validator) do
{:error, "expected #{inspect(parent_key)} to be a list, got: #{inspect(opts)}"}
end
@spec validate!(opts :: keyword(), validator()) :: :ok
def validate!(opts, validator) do
with {:error, reason} <- validator.(opts), do: raise(ArgumentError, reason)
end
@spec validate_schema(opts :: keyword(), schema :: keyword()) :: :ok | {:error, term()}
def validate_schema(opts, schema) when is_list(schema) do
Enum.reduce_while(opts, :ok, fn {key, val}, acc ->
case Keyword.fetch(schema, key) do
{:ok, type} ->
case validate_type(type, key, val) do
:ok -> {:cont, acc}
error -> {:halt, error}
end
:error ->
{:halt, unknown_error(key, Keyword.keys(schema))}
end
end)
end
@spec validate_schema!(opts :: keyword(), schema :: keyword()) :: :ok
def validate_schema!(opts, schema) do
with {:error, reason} <- validate_schema(opts, schema) do
raise ArgumentError, reason
end
end
@doc false
def validate_integer(key, value, opts \\ []) do
min = Keyword.get(opts, :min, 1)
if is_integer(value) and value > min - 1 do
:ok
else
{:error, "expected #{inspect(key)} to be a positive integer, got: #{inspect(value)}"}
end
end
@doc false
def validate_timeout(key, value) do
if (is_integer(value) and value > 0) or value == :infinity do
:ok
else
{:error,
"expected #{inspect(key)} to be a positive integer or :infinity, got: #{inspect(value)}"}
end
end
# Type Validators
defp validate_type(:any, _key, _val), do: :ok
defp validate_type(nil, key, val) do
if is_nil(val) do
:ok
else
{:error, "expected #{inspect(key)} to be nil, got: #{inspect(val)}"}
end
end
defp validate_type(:atom, key, val) do
if is_atom(val) do
:ok
else
{:error, "expected #{inspect(key)} to be an atom, got: #{inspect(val)}"}
end
end
defp validate_type({:behaviour, module}, key, {val, opts}) do
with :ok <- validate_type({:behaviour, module}, key, val) do
if Keyword.keyword?(opts) do
:ok
else
{:error, "expected #{inspect(key)} to be a keyword list, got: #{inspect(opts)}"}
end
end
end
defp validate_type({:behaviour, module}, key, val) do
cond do
not Code.ensure_loaded?(val) ->
{:error, "expected #{inspect(key)} to be a loaded module, got: #{inspect(val)}"}
module not in behaviours(val) ->
{:error, "expected #{inspect(key)} to implement #{inspect(module)}, got: #{inspect(val)}"}
true ->
:ok
end
end
defp validate_type(:boolean, key, val) do
if is_boolean(val) do
:ok
else
{:error, "expected #{inspect(key)} to be a boolean, got: #{inspect(val)}"}
end
end
defp validate_type({:custom, fun}, key, val) when is_function(fun, 1) do
with {:error, error} <- fun.(val) do
message = if is_exception(error), do: error.message, else: error
{:error, "invalid value for #{inspect(key)}, #{message}"}
end
end
defp validate_type({:enum, list}, key, val) do
if val in list do
:ok
else
{:error,
"expected #{inspect(key)} to be included in #{inspect(list)}, got: #{inspect(val)}"}
end
end
defp validate_type(:falsy, key, val) do
if is_nil(val) or val == false do
:ok
else
{:error, "expected #{inspect(key)} to be falsy, got: #{inspect(val)}"}
end
end
defp validate_type({:function, arity}, key, val) do
if is_function(val, arity) do
:ok
else
{:error, "expected #{inspect(key)} to be #{arity} arity function, got: #{inspect(val)}"}
end
end
defp validate_type({:list, type}, key, val) when is_list(val) do
if Enum.all?(val, &(:ok == validate_type(type, key, &1))) do
:ok
else
{:error, "expected #{inspect(key)} to be a list of #{inspect(type)}, got: #{inspect(val)}"}
end
end
defp validate_type({:list, _type}, key, val) do
{:error, "expected #{inspect(key)} to be a list, got: #{inspect(val)}"}
end
defp validate_type(:mfa, key, {module, func, args})
when is_atom(module) and is_atom(func) and is_list(args) do
if function_exported?(module, func, length(args)) do
:ok
else
{:error, "missing function #{Exception.format_mfa(module, func, length(args))} for #{key}"}
end
end
defp validate_type(:mfa, key, val) do
{:error, "expected #{inspect(key)} to be an MFA tuple, got: #{inspect(val)}"}
end
defp validate_type({:module, funs}, key, val) do
cond do
not Code.ensure_loaded?(val) ->
{:error, "expected #{inspect(key)} to be a loaded module, got: #{inspect(val)}"}
not Enum.all?(funs, fn {fun, arity} -> function_exported?(val, fun, arity) end) ->
{:error, "expected #{inspect(key)} to implement #{inspect(funs)}, got: #{inspect(val)}"}
true ->
:ok
end
end
defp validate_type(:non_neg_integer, key, val) do
if is_integer(val) and val >= 0 do
:ok
else
{:error, "expected #{inspect(key)} to be a non negative integer, got: #{inspect(val)}"}
end
end
defp validate_type({:or, types}, key, val) do
if Enum.any?(types, &(:ok == validate_type(&1, key, val))) do
:ok
else
{:error, "expected #{inspect(key)} to be one of #{inspect(types)}, got: #{inspect(val)}"}
end
end
defp validate_type({:pattern, regex}, key, val) do
cond do
not is_binary(val) ->
{:error, "expected #{inspect(key)} to be a binary, got: #{inspect(val)}"}
not Regex.match?(regex, val) ->
{:error, "expected #{inspect(key)} to match #{inspect(regex)}, got: #{inspect(val)}"}
true ->
:ok
end
end
defp validate_type(:pos_integer, key, val) do
if is_integer(val) and val > 0 do
:ok
else
{:error, "expected #{inspect(key)} to be a positive integer, got: #{inspect(val)}"}
end
end
defp validate_type({:range, min..max//_ = range}, key, val) do
if val in range do
:ok
else
{:error, "expected #{inspect(key)} to be between #{min}..#{max}, got: #{inspect(val)}"}
end
end
defp validate_type(:schedule, key, val) do
case Expression.parse(val) do
{:ok, _cron} ->
:ok
{:error, error} ->
{:error, "expected #{inspect(key)} to be a valid cron schedule, got: #{error.message}"}
end
end
defp validate_type(:string, key, val) do
if is_binary(val) do
:ok
else
{:error, "expected #{inspect(key)} to be a string, got: #{inspect(val)}"}
end
end
defp validate_type(:timeout, key, val) do
if (is_integer(val) and val >= 0) or val == :infinity do
:ok
else
{:error,
"expected #{inspect(key)} to be a positive integer or :infinity, got: #{inspect(val)}"}
end
end
defp validate_type(:timezone, key, val) do
if is_binary(val) and match?({:ok, _}, DateTime.now(val)) do
:ok
else
{:error, "expected #{inspect(key)} to be a known timezone, got: #{inspect(val)}"}
end
end
defp validate_type({:tuple, list_of_type}, key, val) when is_tuple(val) do
all_valid? =
val
|> Tuple.to_list()
|> Enum.zip(list_of_type)
|> Enum.all?(fn {sub_val, type} -> :ok == validate_type(type, key, sub_val) end)
if all_valid? do
:ok
else
{:error,
"expected #{inspect(key)} to be a tuple of #{inspect(list_of_type)}, got: #{inspect(val)}"}
end
end
defp validate_type({:tuple, _list_of_type}, key, val) do
{:error, "expected #{inspect(key)} to be a tuple, got: #{inspect(val)}"}
end
defp validate_type(type, _key, _val) do
raise ArgumentError, "unknown schema type: #{inspect(type)}"
end
defp behaviours(module) do
:attributes
|> module.__info__()
|> Keyword.get_values(:behaviour)
|> List.flatten()
end
defp unknown_error(name, known) do
name = to_string(name)
known
|> Enum.map(fn field -> {String.jaro_distance(name, to_string(field)), field} end)
|> Enum.sort(:desc)
|> case do
[{score, field} | _] when score > 0.7 ->
{:error, "unknown option :#{name}, did you mean :#{field}?"}
_ ->
{:error, "unknown option :#{name}"}
end
end
end