Packages

Telegram Bot API client and framework for Elixir

Current section

Files

Jump to
hotline priv generator generate.exs
Raw

priv/generator/generate.exs

defmodule Hotline.Generator do
@moduledoc false
@api_url "https://raw.githubusercontent.com/PaulSonOfLars/telegram-bot-api-spec/main/api.json"
@types_dir "lib/hotline/types"
@generated_api_file "lib/hotline/generated_api.ex"
def run do
IO.puts("Fetching Telegram Bot API spec...")
{:ok, %{status: 200, body: body}} = Req.get(@api_url, decode_body: false)
spec = JSON.decode!(body)
IO.puts("Found #{map_size(spec["types"])} types and #{map_size(spec["methods"])} methods.")
IO.puts("Generating types...")
generate_types(spec["types"])
IO.puts("Generating API methods...")
generate_methods(spec["methods"], spec["types"])
IO.puts("Done! Run `mix format` to format generated files.")
end
# --- Type Generation ---
defp generate_types(types) do
File.mkdir_p!(@types_dir)
for {name, type_def} <- types do
filename = Macro.underscore(name) <> ".ex"
path = Path.join(@types_dir, filename)
content = generate_type_module(name, type_def, types)
File.write!(path, content)
end
end
defp generate_type_module(name, type_def, all_types) do
module_name = "Hotline.Types.#{name}"
fields = type_def["fields"] || []
description = type_def["description"] || ""
field_atoms = Enum.map(fields, fn f -> ":#{f["name"]}" end)
nested = find_nested_fields(fields, all_types)
typespecs = Enum.map(fields, &generate_typespec(&1, all_types))
nested_block =
if nested != [] do
entries =
Enum.map_join(nested, ",\n ", fn {field, parser} -> "#{field}: #{parser}" end)
"""
def parse_nested(_map) do
%{
#{entries}
}
end
"""
else
""
end
typespec_block =
if typespecs != [] do
entries = Enum.map_join(typespecs, ",\n ", fn {n, s} -> "#{n}: #{s}" end)
"""
@type t :: %__MODULE__{
#{entries}
}
"""
else
" @type t :: %__MODULE__{}\n"
end
"""
defmodule #{module_name} do
@moduledoc \"\"\"
#{String.replace(description, "\"\"\"", ~S(\\\"\\\"\\\""))}
\"\"\"
use Hotline.Type
defstruct [#{Enum.join(field_atoms, ", ")}]
#{typespec_block}
#{nested_block}end
"""
end
defp find_nested_fields(fields, all_types) do
for %{"name" => name, "types" => types} <- fields,
parser = find_parser(types, all_types),
parser != nil do
{String.to_atom(name), parser}
end
end
defp find_parser(types, all_types) do
cond do
Enum.any?(types, &match?("Array of " <> _, &1)) ->
inner =
types
|> Enum.find(&match?("Array of " <> _, &1))
|> String.replace_prefix("Array of ", "")
# Handle "Array of Array of X"
inner =
if String.starts_with?(inner, "Array of ") do
String.replace_prefix(inner, "Array of ", "")
else
inner
end
if Map.has_key?(all_types, inner) do
"&Hotline.Types.#{inner}.parse_list/1"
else
nil
end
Enum.any?(types, &Map.has_key?(all_types, &1)) ->
type_name = Enum.find(types, &Map.has_key?(all_types, &1))
"&Hotline.Types.#{type_name}.parse/1"
true ->
nil
end
end
defp generate_typespec(%{"name" => name, "types" => types} = field, all_types) do
required = Map.get(field, "required", false)
spec =
types
|> Enum.map(&map_type(&1, all_types))
|> Enum.uniq()
|> Enum.join(" | ")
spec = if required, do: spec, else: "#{spec} | nil"
{String.to_atom(name), spec}
end
defp map_type("Integer", _), do: "integer()"
defp map_type("String", _), do: "String.t()"
defp map_type("Boolean", _), do: "boolean()"
defp map_type("Float", _), do: "float()"
defp map_type("Float number", _), do: "float()"
defp map_type("True", _), do: "true"
defp map_type("InputFile", _), do: "term()"
defp map_type("Array of " <> inner, all_types) do
"[#{map_type(inner, all_types)}]"
end
defp map_type(type_name, all_types) do
if Map.has_key?(all_types, type_name) do
"Hotline.Types.#{type_name}.t()"
else
"term()"
end
end
# --- Method Generation ---
defp generate_methods(methods, types) do
functions =
methods
|> Enum.sort_by(fn {name, _} -> name end)
|> Enum.map(fn {name, method_def} -> generate_method(name, method_def, types) end)
content = """
defmodule Hotline.GeneratedAPI do
@moduledoc \"\"\"
Auto-generated Telegram Bot API methods.
Generated by `mix hotline.gen` from the official Bot API spec.
\"\"\"
alias Hotline.Client
#{Enum.join(functions, "\n")}end
"""
File.write!(@generated_api_file, content)
end
defp generate_method(name, method_def, types) do
description =
(method_def["description"] || "")
|> String.slice(0..500)
|> String.replace("\"\"\"", ~S(\\\"\\\"\\\""))
returns = method_def["returns"] || []
func_name = Macro.underscore(name)
return_parser = find_return_parser(returns, types)
body =
if return_parser do
"""
with {:ok, result} <- Client.request("#{name}", Map.new(params), opts) do
{:ok, #{return_parser}}
end\
"""
else
"""
Client.request("#{name}", Map.new(params), opts)\
"""
end
"""
@doc \"\"\"
#{description}
\"\"\"
def #{func_name}(params \\\\ %{}, opts \\\\ []) do
#{body}
end
"""
end
defp find_return_parser(returns, types) do
cond do
Enum.any?(returns, &match?("Array of " <> _, &1)) ->
inner =
returns
|> Enum.find(&match?("Array of " <> _, &1))
|> String.replace_prefix("Array of ", "")
if Map.has_key?(types, inner) do
"Hotline.Types.#{inner}.parse_list(result)"
else
nil
end
Enum.any?(returns, &Map.has_key?(types, &1)) ->
type_name = Enum.find(returns, &Map.has_key?(types, &1))
"Hotline.Types.#{type_name}.parse(result)"
true ->
nil
end
end
end
Hotline.Generator.run()