Packages
SuperWorker is a powerful Elixir library for working with supervisors and background jobs. It provides a much simpler approach than traditional supervisors. This library is currently under development and is unstable, so it is not recommended for production use.
Current section
Files
Jump to
Current section
Files
lib/supervisor/validator.ex
defmodule SuperWorker.Supervisor.Validator do
@moduledoc """
Utility functions for SuperWorker.Supervisor.
This module provides common utility functions used across the supervisor
implementation, including:
- Option normalization and validation
- API call helpers
- Hashing and partitioning utilities
- Process information helpers
"""
@sup_params [:id, :number_of_partitions, :link, :report_to, :children]
alias SuperWorker.Supervisor
require Logger
# ============================================================================
# Type Definitions
# ============================================================================
@type api_result :: {:ok, any()} | {:error, any()}
@type timeout_ms :: non_neg_integer() | :infinity
def validate_and_convert(options) do
with {:ok, opts} <- normalize_options(options, @sup_params),
{:ok, opts} <- default_sup_options(opts),
{:ok, opts} <- generic_default_options(opts),
{:ok, opts} <- validate_options(opts),
{:ok, sup} <- to_struct(opts) do
{:ok, sup}
end
end
@spec normalize_options([keyword() | atom()], [atom()]) :: api_result()
def normalize_options(opts, allowed_params) when is_list(opts) and is_list(allowed_params) do
do_normalize_opts(opts, allowed_params, %{}, [])
end
@spec generic_default_options(map()) :: {:ok, map()}
defp generic_default_options(opts) when is_map(opts) do
{:ok, Map.put_new(opts, :owner, self())}
end
@doc """
Checks that the value of a key in options passes a validation function.
## Examples
iex> check_type(%{count: 5}, :count, &is_integer/1)
{:ok, %{count: 5}}
iex> check_type(%{count: "five"}, :count, &is_integer/1)
{:error, :invalid_type}
"""
@spec check_type(map(), atom(), (any() -> boolean())) :: api_result()
def check_type(opts, key, validator_fun)
when is_map(opts) and is_atom(key) and is_function(validator_fun, 1) do
case Map.get(opts, key) do
nil ->
Logger.warning("SuperWorker, Utils, option #{inspect(key)} not found")
{:error, :invalid_type}
value ->
if validator_fun.(value) do
{:ok, opts}
else
Logger.warning("SuperWorker, Utils, option #{inspect(key)} has invalid type")
{:error, :invalid_type}
end
end
end
@doc """
Converts a keyword option shorthand to a full keyword tuple.
Used for converting shorthand like `:group` to `{:type, :group}`.
"""
@spec get_keyword(atom()) :: {:type, atom()} | {:error, :invalid_options}
def get_keyword(:group), do: {:type, :group}
def get_keyword(:chain), do: {:type, :chain}
def get_keyword(:standalone), do: {:type, :standalone}
def get_keyword(_), do: {:error, :invalid_options}
# ============================================================================
# Hashing and Partitioning
# ============================================================================
@doc """
Computes a hash-based order for partitioning data.
Uses Erlang's phash2 for consistent hashing across the cluster.
## Examples
iex> order = get_hash_order("my_data", 10)
iex> order >= 0 and order < 10
true
"""
@spec get_hash_order(term(), pos_integer()) :: non_neg_integer()
def get_hash_order(term, num_partitions)
when is_integer(num_partitions) and num_partitions > 0 do
:erlang.phash2(term, num_partitions)
end
# ============================================================================
# Private Functions
# ============================================================================
# Recursively processes options list, separating valid from invalid options
@spec do_normalize_opts(
[keyword() | atom()],
[atom()],
map(),
[atom() | {atom(), any()}]
) :: api_result()
defp do_normalize_opts([], _allowed_params, valid, invalid) do
if Enum.empty?(invalid) do
{:ok, valid}
else
{:error, {:invalid_options, Enum.reverse(invalid)}}
end
end
# Handle keyword tuple option
defp do_normalize_opts([{key, value} | rest], allowed_params, valid, invalid)
when is_atom(key) do
if key in allowed_params do
do_normalize_opts(rest, allowed_params, Map.put(valid, key, value), invalid)
else
do_normalize_opts(rest, allowed_params, valid, [key | invalid])
end
end
# Handle atom shorthand option (like :group, :chain)
defp do_normalize_opts([short_opt | rest], allowed_params, valid, invalid)
when is_atom(short_opt) do
case get_keyword(short_opt) do
{:type, value} ->
if :type in allowed_params do
do_normalize_opts(rest, allowed_params, Map.put(valid, :type, value), invalid)
else
do_normalize_opts(rest, allowed_params, valid, [short_opt | invalid])
end
{:error, _} ->
if short_opt in allowed_params do
# It's a valid boolean flag
do_normalize_opts(rest, allowed_params, Map.put(valid, short_opt, true), invalid)
else
do_normalize_opts(rest, allowed_params, valid, [short_opt | invalid])
end
end
end
# Handle any other unexpected option format
defp do_normalize_opts([opt | rest], allowed_params, valid, invalid) do
do_normalize_opts(rest, allowed_params, valid, [opt | invalid])
end
# Set the default options if not provided.
# TO-DO: Merge with generic_default_sup_opts/1.
defp default_sup_options(options) do
options =
if Map.has_key?(options, :num_partitions) do
options
else
Map.put(options, :num_partitions, :erlang.system_info(:schedulers_online))
end
options =
if Map.has_key?(options, :link) do
options
else
Map.put(options, :link, true)
end
options =
if Map.has_key?(options, :name) do
options
else
Map.put(options, :name, SuperWorker.Supervisor)
end
options = Map.put(options, :master, options.id)
{:ok, options}
end
# Validate the type & value of options.
defp validate_options(options) do
with {:ok, opts} <- check_type(options, :id, &is_atom/1),
{:ok, opts} <- check_type(opts, :num_partitions, &is_integer/1),
{:ok, opts} <- check_type(opts, :num_partitions, &(&1 > 0)),
{:ok, opts} <- check_type(opts, :owner, &is_pid/1),
{:ok, opts} <- check_type(opts, :link, &is_boolean/1) do
{:ok, opts}
else
{:error, reason} = error ->
Logger.error("SuperWorker, Supervisor, error in validating options: #{inspect(reason)}")
error
end
end
defp to_struct(map_options) when is_map(map_options) do
fields =
%Supervisor{id: nil}
|> Map.from_struct()
|> Map.keys()
result =
Enum.reduce(fields, %Supervisor{id: nil}, fn field, acc ->
if Map.has_key?(map_options, field) do
%{acc | field => Map.get(map_options, field)}
else
acc
end
end)
{:ok, result}
end
end