Current section
Files
Jump to
Current section
Files
lib/nadia/input_text_message_content.ex
defmodule Nadia.InputTextMessageContent do
@moduledoc """
Typed builder for Telegram `InputTextMessageContent`.
Message text must contain 1 to 4096 valid UTF-8 characters. Optional
formatting can be supplied with either `:parse_mode` or `:entities`, but not
both. `:link_preview_options` accepts the current fixed Telegram
`LinkPreviewOptions` shape and preserves explicit `false` values.
"""
@enforce_keys [:fields]
defstruct [:fields]
@typedoc "Typed inline-query content containing a text message."
@opaque t :: %__MODULE__{fields: map}
@type options :: keyword | map
@optional_fields [:parse_mode, :entities, :link_preview_options]
@allowed_fields [:message_text | @optional_fields]
@link_preview_fields [
:is_disabled,
:url,
:prefer_small_media,
:prefer_large_media,
:show_above_text
]
@link_preview_boolean_fields [
:is_disabled,
:prefer_small_media,
:prefer_large_media,
:show_above_text
]
@doc """
Builds text content for an inline-query result.
Supported options are `:parse_mode`, `:entities`, and
`:link_preview_options`. Options whose value is `nil` are omitted.
"""
@spec new(binary, options) :: t
def new(message_text, options \\ []) do
fields =
options
|> normalize_options!()
|> Enum.reduce(%{message_text: message_text}, fn {key, value}, fields ->
if key in @optional_fields do
if is_nil(value), do: fields, else: Map.put(fields, key, value)
else
raise ArgumentError, "unsupported Nadia.InputTextMessageContent option: #{inspect(key)}"
end
end)
content = %__MODULE__{fields: fields}
case to_map(content) do
{:ok, _map} -> content
{:error, reason} -> raise ArgumentError, error_message(reason)
end
end
@doc false
@spec to_map(t) :: {:ok, map} | {:error, term}
def to_map(%__MODULE__{fields: fields} = content) do
with :ok <- validate_struct_fields(content),
:ok <- validate_fields_map(fields),
:ok <- validate_allowed_fields(fields),
:ok <- validate_message_text(fields[:message_text]),
:ok <- validate_parse_mode(fields[:parse_mode]),
:ok <- validate_entities(fields[:entities]),
:ok <- validate_formatting(fields),
{:ok, link_preview_options} <-
normalize_link_preview_options(fields[:link_preview_options]) do
fields =
if Map.has_key?(fields, :link_preview_options) do
Map.put(fields, :link_preview_options, link_preview_options)
else
fields
end
{:ok, reject_nil_values(fields)}
end
end
def to_map(_content), do: {:error, :invalid_input_text_message_content}
defp normalize_options!(options) when is_map(options), do: Map.to_list(options)
defp normalize_options!(options) when is_list(options) do
if Keyword.keyword?(options) do
options
else
raise ArgumentError, "Nadia.InputTextMessageContent options must be a keyword list or map"
end
end
defp normalize_options!(_options),
do:
raise(ArgumentError, "Nadia.InputTextMessageContent options must be a keyword list or map")
defp validate_struct_fields(content) do
case content
|> Map.keys()
|> Enum.sort()
|> Enum.find(&(&1 not in [:__struct__, :fields])) do
nil -> :ok
field -> {:error, {:unsupported_field, field}}
end
end
defp validate_fields_map(fields) when is_map(fields), do: :ok
defp validate_fields_map(fields), do: {:error, {:invalid_fields, fields}}
defp validate_allowed_fields(fields) do
case fields
|> Map.keys()
|> Enum.sort()
|> Enum.find(&(&1 not in @allowed_fields)) do
nil -> :ok
field -> {:error, {:unsupported_field, field}}
end
end
defp validate_message_text(message_text) when is_binary(message_text) do
if String.valid?(message_text) and String.length(message_text) in 1..4096 do
:ok
else
{:error, :invalid_message_text}
end
end
defp validate_message_text(_message_text), do: {:error, :invalid_message_text}
defp validate_parse_mode(nil), do: :ok
defp validate_parse_mode(parse_mode) when is_binary(parse_mode) and byte_size(parse_mode) > 0 do
if String.valid?(parse_mode), do: :ok, else: {:error, :invalid_parse_mode}
end
defp validate_parse_mode(_parse_mode), do: {:error, :invalid_parse_mode}
defp validate_entities(nil), do: :ok
defp validate_entities(entities) when is_list(entities), do: :ok
defp validate_entities(_entities), do: {:error, :invalid_entities}
defp validate_formatting(%{parse_mode: parse_mode, entities: entities})
when not is_nil(parse_mode) and not is_nil(entities),
do: {:error, :text_formatting_conflict}
defp validate_formatting(_fields), do: :ok
defp normalize_link_preview_options(nil), do: {:ok, nil}
defp normalize_link_preview_options(options) when is_list(options) do
if Keyword.keyword?(options) do
options
|> Map.new()
|> normalize_link_preview_options()
else
{:error, :invalid_link_preview_options}
end
end
defp normalize_link_preview_options(%_{} = options) do
options
|> Map.from_struct()
|> normalize_link_preview_options()
end
defp normalize_link_preview_options(options) when is_map(options) do
options = reject_nil_values(options)
with :ok <- validate_link_preview_fields(options),
:ok <- validate_link_preview_booleans(options),
:ok <- validate_link_preview_url(options[:url]) do
{:ok, options}
end
end
defp normalize_link_preview_options(_options), do: {:error, :invalid_link_preview_options}
defp validate_link_preview_fields(options) do
case options
|> Map.keys()
|> Enum.sort()
|> Enum.find(&(&1 not in @link_preview_fields)) do
nil -> :ok
field -> {:error, {:link_preview_options, {:unsupported_field, field}}}
end
end
defp validate_link_preview_booleans(options) do
Enum.reduce_while(@link_preview_boolean_fields, :ok, fn field, :ok ->
case Map.fetch(options, field) do
:error ->
{:cont, :ok}
{:ok, value} when is_boolean(value) ->
{:cont, :ok}
{:ok, value} ->
{:halt, {:error, {:link_preview_options, {:boolean_required, field, value}}}}
end
end)
end
defp validate_link_preview_url(nil), do: :ok
defp validate_link_preview_url(url) when is_binary(url) do
if String.valid?(url), do: :ok, else: {:error, {:link_preview_options, :invalid_url}}
end
defp validate_link_preview_url(_url), do: {:error, {:link_preview_options, :invalid_url}}
defp reject_nil_values(map) do
for {key, value} <- map, not is_nil(value), into: %{}, do: {key, value}
end
defp error_message(:invalid_message_text),
do: "Nadia.InputTextMessageContent message_text must be valid UTF-8 with 1 to 4096 characters"
defp error_message(:invalid_parse_mode),
do: "Nadia.InputTextMessageContent parse_mode must be a non-empty valid UTF-8 binary"
defp error_message(:invalid_entities),
do: "Nadia.InputTextMessageContent entities must be a list"
defp error_message(:text_formatting_conflict),
do: "Nadia.InputTextMessageContent parse_mode and entities are mutually exclusive"
defp error_message(:invalid_link_preview_options),
do:
"Nadia.InputTextMessageContent link_preview_options must be a keyword list, map, or struct"
defp error_message({:link_preview_options, {:unsupported_field, field}}),
do:
"Nadia.InputTextMessageContent link_preview_options does not support field #{inspect(field)}"
defp error_message({:link_preview_options, {:boolean_required, field, _value}}),
do: "Nadia.InputTextMessageContent link_preview_options #{field} must be a boolean"
defp error_message({:link_preview_options, :invalid_url}),
do: "Nadia.InputTextMessageContent link_preview_options url must be valid UTF-8"
defp error_message(reason),
do: "invalid Nadia.InputTextMessageContent value: #{inspect(reason)}"
end