Packages
phoenix_gen_api
0.0.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
@doc """
Config struct for external request.
Request after receive from client will check config and forward to target service.
## Example
```Elixir
%FunConfig{
request_type: "get_user",
service: :user,
nodes: ["user1", "user2"],
choose_node_mode: :random,
timeout: 1000,
mfa: {User, :get_user, []},
arg_types: %{
"user_id" => :string,
"device_id" => :string,
},
arg_orders: ["user_id", "device_id"],
response_type: :async,
}
```
"""
alias __MODULE__
alias PhoenixGenApi.Structs.Request
# default max string length for data from client.
@default_string_max_length 1000
@default_list_max_items 1000
@default_map_max_items 1000
require Logger
defstruct [
# string, an unique identifier for the type of request & response.
:request_type,
# service.
:service,
# list, nodes that will handle the request.
# for local service run same node set to :local
:nodes,
# way to chose node, support: :random, :hash, :round_robin,
:choose_node_mode,
# the timeout for the request.
:timeout,
# the module, function, and arguments that will be called to handle the request.
:mfa,
# map, field -> type, the types of the arguments that will be passed to the function. For validation & converting.
# Support follow types:
# :string - String type.
# :boolean - boolean type.
# :num - number type (integer or float).
# :list_string - list of string.
# :list_num - list of number.
# :list - mix list, ex: [1, "a", true].
# :map - map type. Not support nested map yet.
:arg_types,
# list, the order of the arguments that will be passed to the function.
:arg_orders,
# indicates if the request has a response. Type of response: :sync, :async, :none.
# :sync -> the response will call directly & send back to the client.
# :async -> Add to queue for other process to handle and send back to the client.
# :none -> no response needed, using for updating or sending notification.
:response_type,
# boolean, indicates if need request info, info will be added to the request in the last argument.
# %{request_id: request_id, user_id: user_id, device_id: device_id}
# user_id is the user_id of the user who made the request.
request_info: false,
# check permission, false or {:arg, arg_name}
# false, ignore basic check permission.
# {:arg, arg_name}, verify if request from user has same user_id in argument.
# arg type need to be :string.
# basic check permission run in gen api service before forward to target service.
# if user_id in request_info not match with user_id in argument, return error.
# other case, please pass request_info to target function for checking.
check_permission: false, # TO-DO: Support more complex permission check.
]
@doc """
Select target based on config.
"""
def get_node(config = %FunConfig{}, request = %Request{}) do
# TO-DO: Implement sticky node selection.
case config.choose_node_mode do
:random -> Enum.random(config.nodes)
:hash -> hash_node(request, config)
:round_robin -> round_robin_node(request, config)
end
end
@doc """
Check if the service is local (run on the same node).
"""
def is_local_service?(config = %FunConfig{}) do
config.nodes == :local
end
@doc """
Validate & Convert request arguments to the correct types.
"""
def convert_args!(config = %FunConfig{}, request = %Request{}) do
validate_args!(config, request)
args = request.args
arg_types = config.arg_types
converted_args = Enum.reduce(args, %{}, fn {name, value}, acc ->
type = arg_types[name]
Map.put(acc, name, convert_arg!(value, type))
end)
if map_size(arg_types) == 1 do
Map.values(converted_args)
else
result = Enum.reduce(config.arg_orders, [], fn name, acc ->
case Map.get(converted_args, name) do
nil ->
Logger.error("common, gen_api, request, missing argument #{inspect name} in #{inspect request.request_type}")
raise "missing argument #{inspect name} in #{inspect request.request_type}"
arg ->
acc ++ [arg]
end
end)
Logger.debug("common, gen_api, request, converted args: #{inspect result}")
result
end
end
@doc """
Validate request arguments.
"""
def validate_args!(config = %FunConfig{}, request = %Request{}) do
args = request.args
arg_types = config.arg_types
if map_size(args) != map_size(arg_types) do
Logger.error("common, gen_api, request, invalid number of arguments for #{inspect request.request_type}, request_id: #{inspect request.request_id}")
raise "invalid number of arguments for #{inspect request.request_type}"
end
config_args = MapSet.new(Map.keys(arg_types))
request_args = MapSet.new(Map.keys(args))
if !MapSet.equal?(config_args, request_args) do
Logger.error("common, gen_api, request, invalid arguments for #{inspect request.request_type}, request_id: #{inspect request.request_id}")
raise "invalid arguments for #{inspect request.request_type}"
end
# Verify argument types & values.
Enum.each(args, fn {name, value} ->
case arg_types[name] do
:list ->
if length(value) > @default_list_max_items do
Logger.error("common, gen_api, request, invalid argument size for #{inspect name} in #{inspect request.request_type}, request_id: #{inspect request.request_id}")
raise "invalid argument size for #{inspect name} in #{inspect request.request_type}"
end
arg_list_validation!(value)
{:list, max_items} ->
if length(value) > max_items do
Logger.error("common, gen_api, request, invalid argument size for #{inspect name} in #{inspect request.request_type}, request_id: #{inspect request.request_id}")
raise "invalid argument size for #{inspect name} in #{inspect request.request_type}"
end
arg_list_validation!(value)
:map -> # TO-DO: Implement more for map type validation.
if map_size(value) > @default_map_max_items do
Logger.error("common, gen_api, request, invalid argument size for #{inspect name} in #{inspect request.request_type}, request_id: #{inspect request.request_id}")
raise "invalid argument size for #{inspect name} in #{inspect request.request_type}"
end
arg_map_validation!(value)
{:map, max_items} -> # TO-DO: Implement more for map type validation.
if map_size(value) > max_items do
Logger.error("common, gen_api, request, invalid argument size for #{inspect name} in #{inspect request.request_type}, request_id: #{inspect request.request_id}")
raise "invalid argument size for #{inspect name} in #{inspect request.request_type}"
end
arg_map_validation!(value)
other ->
arg_validation!(other, value)
end
end)
end
defp arg_map_validation!(value) when is_map(value) do
Enum.each( value, fn {_key, val} ->
cond do
is_boolean(val) ->
:ok
is_float(val) or is_integer(val) ->
:ok
is_binary(val) ->
arg_validation!(:string, val)
is_list(val) ->
arg_validation!(:list, val)
is_map(val) ->
Logger.error("gen_api, request, nested map is not supported yet")
raise "nested map is not supported yet"
end
end)
end
defp arg_list_validation!(value) when is_list(value) do
Enum.each( value, fn item->
cond do
is_boolean(item) ->
:ok
is_float(item) or is_integer(item) ->
:ok
is_binary(item) ->
arg_validation!(:string, item)
is_map(item) ->
Logger.error("gen_api, request, nested map is not supported yet")
raise "nested map is not supported yet"
is_list(item) ->
Logger.error("gen_api, request, nested list is not supported yet")
raise "nested list is not supported yet"
true ->
Logger.error("gen_api, request, unsupported type #{inspect item}")
raise "unsupported type #{inspect item}"
end
end)
end
defp arg_list_validation!(value, fn_valid?) when is_list(value) do
Enum.each( value, fn item->
if fn_valid?.(item) do
:ok
else
Logger.error("gen_api, request, unsupported type #{inspect item} in list")
raise "unsupported type of #{inspect item} in list"
end
end)
end
defp arg_validation!(type, value) do
case type do
nil ->
Logger.error("gen_api, request, invalid argument type for #{inspect value}")
raise "invalid argument type for #{inspect value}"
:boolean ->
if value in [true, false] do
:ok
else
Logger.error("gen_api, request, invalid argument type for #{inspect value}")
raise "invalid argument type for #{inspect value}"
end
:num ->
if is_float(value) or is_integer(value) do
:ok
else
Logger.error("gen_api, request, invalid argument type for #{inspect value}")
raise "invalid argument type for #{inspect value}"
end
:string ->
if String.length(value) > @default_string_max_length do
Logger.error("gen_api, request, invalid argument size for #{inspect value}")
raise "invalid argument size for #{inspect value}"
end
{:string, max_length} ->
if String.length(value) > max_length do
Logger.error("gen_api, request, invalid argument size for #{inspect value}")
raise "invalid argument size for #{inspect value}"
end
:list ->
if length(value) > @default_list_max_items do
Logger.error("gen_api, request, invalid argument size for #{inspect value}")
raise "invalid argument size for #{inspect value}"
end
arg_list_validation!(value)
:list_string ->
if length(value) > @default_list_max_items do
Logger.error("gen_api, request, invalid argument size for #{inspect value}")
raise "invalid argument size for #{inspect value}"
end
arg_list_validation!(value, fn item -> is_binary(item) and String.length(item) < @default_string_max_length end)
{:list_string, max_items, max_item_length} ->
if length(value) > max_items do
Logger.error("gen_api, request, invalid argument size for #{inspect value}")
raise "invalid argument size for #{inspect value}"
end
arg_list_validation!(value, fn item -> is_binary(item) and String.length(item) < max_item_length end)
:list_num ->
if length(value) > @default_list_max_items do
Logger.error("gen_api, request, invalid argument size for #{inspect value}")
raise "invalid argument size for #{inspect value}"
end
arg_list_validation!(value, fn item -> is_float(item) or is_integer(item) end)
{:list_num, max_items} ->
if length(value) > max_items do
Logger.error("gen_api, request, invalid argument size for #{inspect value}")
raise "invalid argument size for #{inspect value}"
end
arg_list_validation!(value, fn item -> is_float(item) or is_integer(item) end)
_ ->
Logger.error("gen_api, request, unsupported type #{inspect type} for #{inspect value}")
raise "unsupported type #{inspect type} for #{inspect value}"
end
end
## private functions ##
defp hash_node(request, config) do
hash_order = :erlang.phash2(request.request_id, length(config.nodes))
Enum.at(config.nodes, hash_order)
end
defp round_robin_node(_request, config) do
# TO-DO: Implement round robin node selection.
Enum.at(config.nodes, 0)
end
defp convert_arg!(arg, :string) when is_binary(arg) do
arg
end
defp convert_arg!(arg, :boolean) when arg in [true, false] do
arg
end
defp convert_arg!(arg, :num) when is_float(arg) or is_integer(arg) do
arg
end
defp convert_arg!(arg, :list_string) when is_list(arg) do
Enum.each(arg, fn
x when is_binary(x) -> x
x -> raise "invalid type #{inspect x} for list_string"
end)
arg
end
defp convert_arg!(arg, :list_num) when is_list(arg) do
Enum.each(arg, fn
x when is_float(x) or is_integer(x) -> x
x -> raise "invalid type #{inspect x} for list_num"
end)
arg
end
defp convert_arg!(arg, :list) when is_list(arg) do
arg
end
defp convert_arg!(arg, :map) when is_map(arg) do
arg
end
defp convert_arg!(arg, type) do
Logger.error("gen_api, request, unsupported type #{inspect type} for #{inspect arg}")
raise "unsupported type #{inspect type} for #{inspect arg}"
end
@doc """
Check permission for request from client.
"""
def check_permission(%Request{}, %FunConfig{check_permission: false}) do
true
end
def check_permission(request = %Request{}, %FunConfig{check_permission: {:arg, arg_name}}) do
case Map.get(request.args, arg_name) do
nil ->
Logger.warning("gen_api, check permission, missing argument #{inspect arg_name} in request: #{inspect request}")
false
user_id ->
user_id == request.user_id
end
end
def check_permission!(request = %Request{}, fun_config = %FunConfig{}) do
if not check_permission(request, fun_config) do
Logger.warning(" gen_api, check permission, failed, request: #{inspect request}, fun config: #{inspect fun_config}")
raise "Permission denied for request from user: #{inspect request.user_id}, request: #{inspect request.request_id}"
end
end
end