Current section

Files

Jump to
ex_gram lib ex_gram macros executer.ex
Raw

lib/ex_gram/macros/executer.ex

defmodule ExGram.Macros.Executer do
@moduledoc """
Executer for the method macro, it takes care of checking the parameters, fetching the token, building the path and body, and calling the adapter.
"""
alias ExGram.Macros.Checker
require Logger
# credo:disable-for-next-line
def execute_method(name, verb, body, file_parameters, returned_type, ops, method_ops, mandatory_types, optional_types) do
adapter =
Keyword.get_lazy(ops, :adapter, fn ->
ExGram.Config.get(:ex_gram, :adapter, ExGram.Adapter.Tesla)
end)
token = ExGram.Token.fetch(ops)
debug = Keyword.get(ops, :debug, false)
check_params? =
Keyword.get_lazy(ops, :check_params, fn ->
ExGram.Config.get(:ex_gram, :check_params, true)
end)
with {:token, token} when is_binary(token) <- {:token, token},
{:params, :ok} <-
{:params, check_params(check_params?, mandatory_types, method_ops, optional_types)} do
path = build_path(token, name)
body =
body
|> Keyword.merge(method_ops)
|> Map.new()
|> clean_body()
|> body_with_files(file_parameters)
if debug, do: Logger.info("Path: #{inspect(path)}\nbody: #{inspect(body)}")
adapter_opts = Keyword.get(ops, :adapter_opts, [])
case adapter.request(verb, path, body, adapter_opts) do
{:ok, body} ->
process_result(body, returned_type)
{:error, error} ->
{:error, error}
end
else
{:token, _} ->
message =
~s(No token available in the request, make sure you have the token setup on the config or you used the parameter "token" or "bot" correctly)
{:error,
%ExGram.Error{
message: message
}}
{:params, {:error, msg}} ->
{:error, %ExGram.Error{message: msg}}
end
end
defp build_path(token, name) do
token_part = "/bot#{token}"
if_result =
if ExGram.test_environment?() do
[token_part, "test", name]
else
[token_part, name]
end
Path.join(if_result)
end
defp body_with_files(body, file_parts) do
{input_media_params, direct_params} =
Enum.split_with(file_parts, fn
{:input_media, _name} -> true
_ -> false
end)
direct_file_parts =
direct_params
|> Enum.map(fn
{v, p} -> {v, p}
keyw -> {body[keyw], Atom.to_string(keyw)}
end)
|> Enum.filter(fn
{parameter, _} when is_tuple(parameter) -> elem(parameter, 0) in [:file, :file_content]
_ -> false
end)
|> Enum.map(fn
{{:file, path}, partname} ->
{:file, partname, path}
{{:file_content, content, filename}, partname} ->
{:file_content, partname, content, filename}
end)
{body, media_file_parts} =
Enum.reduce(input_media_params, {body, []}, fn {:input_media, name}, {body, files} ->
case Map.get(body, name) do
nil ->
{body, files}
media_value ->
{updated_media, extracted_files} = extract_media_files(media_value, name)
{Map.put(body, name, updated_media), files ++ extracted_files}
end
end)
create_multipart(body, direct_file_parts ++ media_file_parts)
end
@input_media_file_fields [:media, :thumbnail, :cover]
defp extract_media_files(media_list, param_name) when is_list(media_list) do
{updated_items, all_files} =
media_list
|> Enum.with_index()
|> Enum.map_reduce([], fn {item, index}, acc_files ->
{updated_item, files} = extract_files_from_media_item(item, param_name, index)
{updated_item, acc_files ++ files}
end)
{updated_items, all_files}
end
defp extract_media_files(media_item, param_name) when is_map(media_item) do
{updated_item, files} = extract_files_from_media_item(media_item, param_name, 0)
{updated_item, files}
end
defp extract_media_files(other, _param_name), do: {other, []}
defp extract_files_from_media_item(item, param_name, index) when is_map(item) do
Enum.reduce(@input_media_file_fields, {item, []}, fn field, {item, files} ->
attach_name = "#{param_name}_#{index}_#{field}"
case Map.get(item, field) do
{:file, path} ->
updated_item = Map.put(item, field, "attach://#{attach_name}")
file_part = {:file, attach_name, path}
{updated_item, files ++ [file_part]}
{:file_content, content, filename} ->
updated_item = Map.put(item, field, "attach://#{attach_name}")
file_part = {:file_content, attach_name, content, filename}
{updated_item, files ++ [file_part]}
_ ->
{item, files}
end
end)
end
defp extract_files_from_media_item(item, _param_name, _index), do: {item, []}
defp to_size_string(true), do: "true"
defp to_size_string(false), do: "false"
defp to_size_string(x) when is_binary(x), do: x
defp to_size_string(x) when is_integer(x), do: Integer.to_string(x)
# This is useful to encode automatically
defp to_size_string(x) when is_map(x), do: encode(x)
defp to_size_string(x) when is_list(x), do: encode(x)
defp to_size_string(_), do: raise("Not sizable!")
defp encode(%{__struct__: _} = x) do
x
|> Map.from_struct()
|> clean_body()
|> ExGram.Encoder.encode!()
end
defp encode(x) when is_map(x) or is_list(x), do: ExGram.Encoder.encode!(x)
defp encode(x), do: x
defp clean_body(%{__struct__: _} = m) do
m |> Map.from_struct() |> clean_body()
end
defp clean_body(map) when is_map(map) do
for {k, v} <- map, not is_nil(v), into: %{}, do: {k, clean_body(v)}
end
defp clean_body(m) when is_list(m), do: Enum.map(m, &clean_body/1)
defp clean_body(m), do: m
defp process_result(result, type) do
ExGram.Cast.cast(result, type)
end
defp create_multipart(body, []), do: body
defp create_multipart(body, fileparts) do
filepart_names =
fileparts
|> Enum.map(&elem(&1, 1))
|> Enum.reduce([], fn name, acc ->
case safe_to_existing_atom(name) do
{:ok, atom} -> [atom | acc]
:error -> acc
end
end)
restparts =
body
|> Map.drop(filepart_names)
|> Map.to_list()
|> Enum.map(fn {name, value} ->
{Atom.to_string(name), to_size_string(value)}
end)
parts = fileparts ++ restparts
{:multipart, parts}
end
defp safe_to_existing_atom(name) when is_binary(name) do
{:ok, String.to_existing_atom(name)}
rescue
ArgumentError -> :error
end
defp check_params(false, _mandatory, _optional, _optional_types), do: :ok
defp check_params(true, mandatory, optional, optional_types) do
mandatory_checks = mandatory |> Checker.check_types() |> mandatory_errors()
optional =
Enum.map(optional, fn {key, value} -> {value, Keyword.get(optional_types, key), key} end)
optional_checks =
optional
|> Enum.map(fn {value, types, _key} -> [value, types] end)
|> Checker.check_types()
|> optional_errors(optional)
case {mandatory_checks, optional_checks} do
{:ok, :ok} -> :ok
{:ok, msg} -> {:error, msg}
{msg, :ok} -> {:error, msg}
{msg, msg2} -> {:error, "#{msg}\n#{msg2}"}
end
end
defp mandatory_errors(:ok), do: :ok
defp mandatory_errors({:error, errors}) do
msg =
Enum.map_join(errors, ", ", fn {{value, types}, index} ->
expected_type_msg(index, types, value)
end)
"Mandatory parameter types don't match: #{msg}"
end
defp optional_errors(:ok, _optional), do: :ok
defp optional_errors({:error, errors}, optional) do
msg =
Enum.map_join(errors, ", ", fn {{value, types}, index} ->
{_, _, name} = Enum.at(optional, index)
expected_type_msg(name, types, value)
end)
"Optional parameter types don't match: #{msg}"
end
defp expected_type_msg(name, types, value) do
"parameter #{name} expected #{inspect(types)} but got #{inspect(value)}"
end
end