Packages
oban
2.23.0
2.23.0
2.22.1
2.22.0
2.21.1
2.21.0
2.20.3
2.20.2
2.20.1
2.20.0
2.19.4
2.19.3
2.19.2
2.19.1
2.19.0
2.18.3
2.18.2
2.18.1
2.18.0
2.17.12
2.17.11
2.17.10
2.17.9
2.17.8
2.17.7
2.17.6
2.17.5
2.17.4
2.17.3
2.17.2
2.17.1
2.17.0
2.16.3
2.16.2
2.16.1
2.16.0
2.15.4
2.15.3
2.15.2
2.15.1
2.15.0
2.14.2
2.14.1
2.14.0
2.13.6
2.13.5
2.13.4
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.3
2.11.2
2.11.1
2.11.0
2.10.1
2.10.0
retired
2.9.2
2.9.1
2.9.0
2.8.0
2.7.2
2.7.1
2.7.0
2.6.1
2.6.0
2.5.0
2.4.3
2.4.2
2.4.1
2.4.0
2.3.4
2.3.3
2.3.2
2.3.1
2.3.0
2.2.0
2.1.0
2.0.0
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
1.2.0
1.1.0
1.0.0
1.0.0-rc.2
1.0.0-rc.1
0.12.1
0.12.0
0.11.1
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
Robust job processing, backed by modern PostgreSQL, SQLite3, and MySQL.
Current section
Files
Jump to
Current section
Files
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