Packages
phoenix_gen_api
2.20.1
2.22.0
2.21.1
2.21.0
2.20.1
2.20.0
2.19.0
2.18.1
2.18.0
2.17.1
2.17.0
2.16.0
2.15.0
2.14.0
2.13.0
2.12.0
2.11.0
2.10.0
2.9.1
2.9.0
2.8.0
2.7.0
2.6.1
2.6.0
2.5.0
2.4.0
2.3.0
2.2.0
2.1.1
2.1.0
2.0.1
2.0.0
1.2.0
1.1.3
1.1.2
1.1.1
1.1.0
1.0.1
1.0.0
0.2.1
0.2.0
0.1.1
0.1.0
0.0.16
0.0.15
0.0.14
0.0.13
0.0.12
0.0.11
0.0.10
0.0.9
0.0.8
0.0.7
0.0.6
0.0.5
0.0.4
0.0.3
0.0.2
0.0.1
A library for fast develop APIs in Elixir cluster, using Phoenix Channels for transport data, auto pull api configs from service nodes.
Current section
Files
Jump to
Current section
Files
lib/phoenix_gen_api/structs/fun_config.ex
defmodule PhoenixGenApi.Structs.FunConfig do
@moduledoc """
Defines the configuration for a function that can be called through the API.
This struct holds all the necessary information to route, validate, and execute
a function call based on an incoming request.
## Version
The `version` field is a string (e.g., "1.0.0"). The value `"0.0.0"` is reserved
as a sentinel and cannot be explicitly registered — it is used internally to
mean "no version specified". If a config has no version set, `version/1` returns
`nil` and the config is stored with a `nil` version key in the cache.
## Argument Types (arg_types)
The `arg_types` field supports two formats:
### Simple Format (Backward Compatible)
Uses just the type atom:
arg_types: %{"user_id" => :string, "age" => :num}
### Extended Format (New)
Uses a keyword list with `:type` and optional parameters:
arg_types: %{
"user_id" => [type: :string, max_bytes: 255, allow_nil?: true],
"age" => [type: :num, default_value: 18],
"tags" => [type: :list_string, max_items: 10, max_item_bytes: 100],
"gps_list" => [type: :list_map, max_items: 100],
"metadata" => [type: :map, max_items: 200, required: ["name"], accept: ["name", "email", "age"]]
}
#### Extended Format Options
- `type:` - Required. The argument type (`:string`, `:num`, `:boolean`, etc.)
- `allow_nil?:` - Optional. When `true`, allows nil values (default: `false`)
- `default_value:` - Optional. Default value if argument is missing from request
- `max_bytes:` - For `:string` type, max byte size
- `max_items:` - For list/map types, max number of items
- `max_item_bytes:` - For `:list_string`, max bytes per item
- `required:` - For `:map` type only, list of required key names (e.g., `["name", "email"]`)
- `accept:` - For `:map` type only, list of accepted key names — any key not in this list causes an error
#### Validation
- Default values are validated to match their declared type during config validation
- Invalid default values will cause `FunConfig.validate_with_details/1` to return an error
## Retry
The `retry` field configures retry behavior when request execution fails
(returns `{:error, _}` or `{:error, _, _}`).
Possible values:
- `nil` - No retry (default, backward compatible)
- A positive number (e.g., `3`) - Equivalent to `{:all_nodes, 3}`.
Retry across all available nodes.
- `{:same_node, positive_number}` (e.g., `{:same_node, 2}`) - Retry on the
same node(s) that were originally selected by the `choose_node_mode` strategy.
Useful when the failure might be transient.
- `{:all_nodes, positive_number}` (e.g., `{:all_nodes, 3}`) - Retry across
all available nodes in the cluster. Useful when a node might be down.
For `nodes: :local`, both `:same_node` and `:all_nodes` retry on the same
local machine since there's only one node.
Use `normalize_retry/1` to convert a raw config value to the standard tuple
format. Zero, negative numbers, strings, and other formats are invalid.
## Hooks
The `before_execute` and `after_execute` fields allow you to run custom code
before and/or after a function is executed through the API. Hooks are specified
as MFA tuples:
- `{module, function}` — Called as `module.function(request, fun_config)` (before)
or `module.function(request, fun_config, result)` (after).
- `{module, function, extra_args}` — Extra arguments are appended.
### Before execute
Must return one of:
- `{:ok, request, fun_config}` — Proceed with (possibly modified) request/config.
- `{:error, reason}` — Abort execution and return an error response.
### After execute
Must return the (possibly modified) result. Any other return value is ignored and
the original result is preserved.
Hooks emit telemetry events at `[:phoenix_gen_api, :hook, :before|:after, :start|:stop|:exception]`.
## Permission Callback
The `permission_callback` field allows a custom MFA to override the built-in
`check_permission` modes. When set to `{module, function, args}` (or
`{module, function}`), it is called as `apply(module, function, [request | args])`
and must return `true` or `false`. Any other return, exception, or catch is
treated as `false` (denied). When `permission_callback` is set, `check_permission`
is ignored.
## Security Considerations
- MFA tuples are validated to ensure modules are loaded and functions exist
- Node lists are validated to prevent routing to invalid destinations
- Permission modes are checked against available request fields
- Timeout values are bounded to prevent resource exhaustion
- Hook failures in `before_execute` abort the request; hook failures in `after_execute`
are silently ignored (the original result is preserved)
- Permission callbacks that raise exceptions are treated as denied (fail-closed)
"""
alias PhoenixGenApi.ArgumentHandler
alias PhoenixGenApi.NodeSelector
alias PhoenixGenApi.Permission
alias PhoenixGenApi.Structs.Request
require Logger
@max_timeout 300_000
@min_timeout 100
@type t :: %__MODULE__{
request_type: String.t(),
service: atom() | String.t(),
nodes: list(atom()) | {module(), function(), args :: list()} | :local,
choose_node_mode:
:random | :hash | {:hash, String.t()} | :round_robin | {:sticky, String.t()},
timeout: integer() | :infinity,
mfa: {module(), function(), args :: list()},
arg_types: map() | nil,
arg_orders: list(String.t()) | :map,
response_type: :sync | :async | :stream | :none,
check_permission: false | :any_authenticated | {:arg, String.t()} | {:role, list()},
permission_callback: {module(), atom(), args :: list()} | nil,
request_info: boolean(),
version: String.t(),
disabled: boolean,
retry: {:same_node, number()} | {:all_nodes, number()} | number() | nil,
before_execute: {module(), atom()} | {module(), atom(), args :: list()} | nil,
after_execute: {module(), atom()} | {module(), atom(), args :: list()} | nil,
hook_timeout: pos_integer()
}
@no_version_sentinel "0.0.0"
defstruct [
:request_type,
:service,
:nodes,
:choose_node_mode,
:timeout,
:mfa,
:arg_types,
:arg_orders,
:response_type,
request_info: false,
check_permission: false,
permission_callback: nil,
version: nil,
disabled: false,
retry: nil,
before_execute: nil,
after_execute: nil,
hook_timeout: 5000
]
@doc """
Returns the version of the function configuration.
If the version is not set, is `"0.0.0"` (reserved sentinel), or is empty,
returns `nil` to indicate no version was specified.
"""
@spec version(t()) :: String.t() | nil
def version(config = %__MODULE__{}) do
case Map.get(config, :version) do
version when is_binary(version) and byte_size(version) > 0 and version != "0.0.0" ->
version
_ ->
nil
end
end
@doc """
Selects a target node for the request based on the `choose_node_mode` strategy.
"""
def get_node(config = %__MODULE__{}, request = %Request{}) do
NodeSelector.get_node(config, request)
end
@doc """
Checks if the service is configured to run locally.
"""
def local_service?(config = %__MODULE__{}) do
config.nodes == :local
end
@doc """
Validates and converts the request arguments based on the `arg_types` and `arg_orders` configuration.
"""
def convert_args!(config = %__MODULE__{}, request = %Request{}) do
ArgumentHandler.convert_args!(config, request)
end
@doc """
Checks if the request has the necessary permissions to be executed.
"""
def check_permission!(request = %Request{}, config = %__MODULE__{}) do
Permission.check_permission!(request, config)
end
@doc """
Validates the function configuration.
Returns `true` if all configuration fields are valid, `false` otherwise.
Logs detailed error messages for each invalid field.
## Validation Checks
- `request_type` must be a non-empty string
- `service` must not be nil
- `nodes` must be a valid list, MFA tuple, or `:local`
- `choose_node_mode` must be a recognized strategy
- `timeout` must be a positive integer or `:infinity`
- `mfa` must be a valid `{module, function, args}` tuple
- `arg_types` and `arg_orders` must be consistent
- `response_type` must be one of `:sync`, `:async`, `:stream`, or `:none`
- `check_permission` must be a valid permission mode
- `request_info` must be a boolean
"""
def valid?(config = %__MODULE__{}) do
case validate_with_details(config) do
{:ok, _} ->
true
{:error, errors} ->
Logger.error(
"[FunConfig] validation failed, errors: #{inspect(errors)}, request_type: #{inspect(config.request_type)}, service: #{inspect(config.service)}, nodes: #{inspect(config.nodes)}, mfa: #{inspect(config.mfa)}, response_type: #{inspect(config.response_type)}, check_permission: #{inspect(config.check_permission)}, retry: #{inspect(config.retry)}, version: #{inspect(config.version)}"
)
false
end
end
@doc """
Validates the function configuration and returns detailed error information.
Returns `{:ok, config}` if valid, or `{:error, [error_messages]}` if invalid.
"""
def validate_with_details(config = %__MODULE__{}) do
validations = [
{valid_request_type?(config.request_type), "request_type must be a non-empty string"},
{config.service != nil, "service must not be nil"},
{valid_nodes?(config.nodes), "nodes must be a valid list, MFA tuple, or :local"},
{valid_choose_node_mode?(config.choose_node_mode),
"choose_node_mode must be :random, :hash, {:hash, key}, :round_robin, or {:sticky, key}"},
{valid_timeout?(config.timeout),
"timeout must be a positive integer between #{@min_timeout} and #{@max_timeout}, or :infinity"},
{valid_mfa?(config.mfa), "mfa must be a valid {module, function, args} tuple"},
{validate_args_details(config.arg_types, config.arg_orders) == :ok,
"argument validation failed"},
{config.response_type in [:sync, :async, :stream, :none],
"response_type must be :sync, :async, :stream, or :none"},
{valid_check_permission?(config.check_permission, config.arg_types),
"check_permission must be false, :any_authenticated, {:arg, arg_name}, or {:role, roles}"},
{valid_permission_callback?(config.permission_callback),
"permission_callback must be nil or a valid {module, function, args} tuple"},
{is_boolean(config.request_info), "request_info must be a boolean"},
{valid_version?(config.version), "version must be a valid version string (e.g., '1.0.0')"},
{valid_retry?(config.retry),
"retry must be nil, a positive number, {:same_node, number}, or {:all_nodes, number}"},
{valid_hook?(config.before_execute),
"before_execute must be nil or a valid {module, function} or {module, function, args} tuple"},
{valid_hook?(config.after_execute),
"after_execute must be nil or a valid {module, function} or {module, function, args} tuple"},
{valid_hook_timeout?(config.hook_timeout), "hook_timeout must be a positive integer"}
]
errors =
Enum.reduce(validations, [], fn {valid?, error_msg}, acc ->
if valid?, do: acc, else: [error_msg | acc]
end)
if Enum.empty?(errors) do
{:ok, config}
else
{:error, Enum.reverse(errors)}
end
end
@doc """
Normalizes the retry configuration into a standard tuple format.
- `nil` remains `nil` (no retry)
- A number `n` is converted to `{:all_nodes, n}`
- `{:same_node, n}` and `{:all_nodes, n}` are returned as-is
## Examples
iex> FunConfig.normalize_retry(nil)
nil
iex> FunConfig.normalize_retry(3)
{:all_nodes, 3}
iex> FunConfig.normalize_retry({:same_node, 2})
{:same_node, 2}
iex> FunConfig.normalize_retry({:all_nodes, 5})
{:all_nodes, 5}
"""
@spec normalize_retry(nil | number() | {:same_node, number()} | {:all_nodes, number()}) ::
nil | {:same_node, pos_integer()} | {:all_nodes, pos_integer()}
def normalize_retry(nil), do: nil
def normalize_retry(n) when is_number(n), do: {:all_nodes, trunc(n)}
def normalize_retry({:same_node, n}) when is_number(n), do: {:same_node, trunc(n)}
def normalize_retry({:all_nodes, n}) when is_number(n), do: {:all_nodes, trunc(n)}
defp validate_args_details(nil, nil), do: :ok
defp validate_args_details(nil, arg_orders)
when arg_orders == [] or arg_orders == nil,
do: :ok
defp validate_args_details(nil, _),
do: {:error, "arg_types is nil but arg_orders is not empty"}
defp validate_args_details(arg_types, arg_orders)
when is_map(arg_types) and map_size(arg_types) == 0 do
if arg_orders == [] or arg_orders == nil do
:ok
else
{:error, "arg_types is empty but arg_orders is not"}
end
end
defp validate_args_details(arg_types, []) when is_map(arg_types) and map_size(arg_types) > 0 do
# Empty arg_orders with populated arg_types means arg order doesn't matter
# Similar to :map mode - just validate each arg config
invalid_args =
Enum.filter(arg_types, fn {_name, arg_config} ->
not valid_arg_config?(arg_config)
end)
if Enum.empty?(invalid_args) do
:ok
else
{:error,
"invalid arg_types configuration for: #{inspect(Enum.map(invalid_args, fn {name, _} -> name end))}"}
end
end
defp validate_args_details(arg_types, arg_orders)
when is_map(arg_types) and is_list(arg_orders) do
if map_size(arg_types) != length(arg_orders) do
{:error,
"arg_types count (#{map_size(arg_types)}) does not match arg_orders count (#{length(arg_orders)})"}
else
args_set = MapSet.new(Map.keys(arg_types))
orders_set = MapSet.new(arg_orders)
if MapSet.equal?(args_set, orders_set) do
# Validate each arg config
invalid_args =
Enum.filter(arg_types, fn {_name, arg_config} ->
not valid_arg_config?(arg_config)
end)
if Enum.empty?(invalid_args) do
:ok
else
{:error,
"invalid arg_types configuration for: #{inspect(Enum.map(invalid_args, fn {name, _} -> name end))}"}
end
else
missing = MapSet.difference(args_set, orders_set) |> MapSet.to_list()
extra = MapSet.difference(orders_set, args_set) |> MapSet.to_list()
{:error, "arg mismatch, missing: #{inspect(missing)}, extra: #{inspect(extra)}"}
end
end
end
defp validate_args_details(arg_types, :map)
when is_map(arg_types) and map_size(arg_types) > 0 do
# Validate each arg config
invalid_args =
Enum.filter(arg_types, fn {_name, arg_config} ->
not valid_arg_config?(arg_config)
end)
if Enum.empty?(invalid_args) do
:ok
else
{:error,
"invalid arg_types configuration for: #{inspect(Enum.map(invalid_args, fn {name, _} -> name end))}"}
end
end
defp validate_args_details(_, _), do: {:error, "invalid arg_types or arg_orders format"}
# Private validation helpers
@doc false
defp valid_request_type?(request_type)
when is_binary(request_type) and byte_size(request_type) > 0 do
true
end
defp valid_request_type?(_), do: false
@doc false
defp valid_nodes?(nodes) when is_list(nodes) do
nodes != [] and Enum.all?(nodes, &PhoenixGenApi.Helpers.Shared.valid_node?/1)
end
defp valid_nodes?(nodes) when is_tuple(nodes) do
case nodes do
{module, function, args} when is_atom(module) and is_atom(function) and is_list(args) ->
true
_ ->
false
end
end
defp valid_nodes?(:local), do: true
defp valid_nodes?(_), do: false
@doc false
defp valid_choose_node_mode?(:random), do: true
defp valid_choose_node_mode?(:hash), do: true
defp valid_choose_node_mode?(:round_robin), do: true
defp valid_choose_node_mode?({:hash, key}) when is_binary(key), do: true
defp valid_choose_node_mode?({:sticky, key}) when is_binary(key), do: true
defp valid_choose_node_mode?(_), do: false
@doc false
defp valid_timeout?(:infinity), do: true
defp valid_timeout?(timeout)
when is_integer(timeout) and timeout >= @min_timeout and timeout <= @max_timeout, do: true
defp valid_timeout?(_), do: false
@doc false
defp valid_mfa?({module, function, args})
when is_atom(module) and is_atom(function) and is_list(args) do
true
end
defp valid_mfa?(_), do: false
@doc false
defp valid_check_permission?(false, _arg_types), do: true
defp valid_check_permission?(:any_authenticated, _arg_types), do: true
defp valid_check_permission?({:arg, arg_name}, _arg_types) when is_binary(arg_name), do: true
defp valid_check_permission?({:role, roles}, _arg_types) when is_list(roles), do: true
defp valid_check_permission?(_, _), do: false
@doc false
defp valid_permission_callback?(nil), do: true
defp valid_permission_callback?({module, function, args})
when is_atom(module) and is_atom(function) and is_list(args), do: true
defp valid_permission_callback?({module, function}) when is_atom(module) and is_atom(function),
do: true
defp valid_permission_callback?(_), do: false
@doc false
defp valid_version?(nil), do: true
defp valid_version?(@no_version_sentinel), do: false
defp valid_version?(version) when is_binary(version) and byte_size(version) > 0, do: true
defp valid_version?(_), do: false
@doc false
defp valid_retry?(nil), do: true
defp valid_retry?(n) when is_number(n) and n > 0, do: true
defp valid_retry?({:same_node, n}) when is_number(n) and n > 0, do: true
defp valid_retry?({:all_nodes, n}) when is_number(n) and n > 0, do: true
defp valid_retry?(_), do: false
@doc false
defp valid_arg_config?(arg_config) when is_atom(arg_config) do
# Simple format - just a type atom
arg_config in [
:string,
:num,
:boolean,
:list_string,
:list_num,
:list_uuid,
:list_map,
:map,
:any,
:uuid
]
end
defp valid_arg_config?(arg_config) when is_tuple(arg_config) do
# Old tuple format: {:string, 255}, {:list, 10}, etc.
case arg_config do
{type, _value} when is_atom(type) ->
type in [
:string,
:num,
:boolean,
:list_string,
:list_num,
:list_uuid,
:list_map,
:map,
:any,
:uuid
]
_ ->
false
end
end
defp valid_arg_config?(arg_config) when is_list(arg_config) do
# Extended format - keyword list with :type required
case Keyword.get(arg_config, :type) do
type
when type in [
:string,
:num,
:boolean,
:list_string,
:list_num,
:list_uuid,
:list_map,
:map,
:any,
:uuid
] ->
true
_ ->
false
end
end
defp valid_arg_config?(_), do: false
@doc false
defp valid_hook?(nil), do: true
defp valid_hook?({module, function}) when is_atom(module) and is_atom(function), do: true
defp valid_hook?({module, function, args})
when is_atom(module) and is_atom(function) and is_list(args), do: true
defp valid_hook?(_), do: false
defp valid_hook_timeout?(timeout) when is_integer(timeout) and timeout > 0, do: true
defp valid_hook_timeout?(_), do: false
end