defmodule Jido.Chat.Postable do
@moduledoc """
Typed post payload accepted by thread/channel post helpers.
"""
alias Jido.Chat.{Attachment, Card, FileUpload, Markdown, PostPayload}
@schema Zoi.struct(
__MODULE__,
%{
kind:
Zoi.enum([:raw, :markdown, :ast, :card, :text, :stream])
|> Zoi.default(:text),
text: Zoi.string() |> Zoi.nullish(),
markdown: Zoi.string() |> Zoi.nullish(),
raw: Zoi.any() |> Zoi.nullish(),
ast: Zoi.any() |> Zoi.nullish(),
card: Zoi.any() |> Zoi.nullish(),
stream: Zoi.any() |> Zoi.nullish(),
fallback_text: Zoi.string() |> Zoi.nullish(),
attachments: Zoi.array(Zoi.struct(Attachment)) |> Zoi.default([]),
files: Zoi.array(Zoi.struct(FileUpload)) |> Zoi.default([]),
metadata: Zoi.map() |> Zoi.default(%{})
},
coerce: true
)
@type t :: unquote(Zoi.type_spec(@schema))
@enforce_keys Zoi.Struct.enforce_keys(@schema)
defstruct Zoi.Struct.struct_fields(@schema)
@doc "Returns the Zoi schema for Postable."
def schema, do: @schema
@doc "Creates a new typed post payload."
def new(%__MODULE__{} = postable), do: postable
def new(attrs) when is_map(attrs) do
attrs
|> PostPayload.new()
|> Map.from_struct()
|> Map.take([
:kind,
:text,
:markdown,
:raw,
:ast,
:card,
:stream,
:fallback_text,
:attachments,
:files,
:metadata
])
|> then(&Jido.Chat.Schema.parse!(__MODULE__, @schema, &1))
end
@doc "Builds a text post payload."
def text(value, opts \\ []) when is_binary(value),
do: new(Map.merge(normalize_opts(opts), %{kind: :text, text: value}))
@doc "Builds a markdown post payload."
def markdown(value, opts \\ [])
def markdown(value, opts) when is_binary(value),
do: new(Map.merge(normalize_opts(opts), %{kind: :markdown, markdown: value, text: value}))
def markdown(%Markdown{} = value, opts) do
new(
Map.merge(normalize_opts(opts), %{
kind: :markdown,
markdown: Markdown.stringify(value),
text: Markdown.plain_text(value)
})
)
end
@doc "Builds a raw payload wrapper."
def raw(value, opts \\ []), do: new(Map.merge(normalize_opts(opts), %{kind: :raw, raw: value}))
@doc "Builds an AST payload wrapper."
def ast(value, opts \\ []), do: new(Map.merge(normalize_opts(opts), %{kind: :ast, ast: value}))
@doc "Builds a card payload wrapper."
def card(value, opts \\ [])
def card(%Card{} = value, opts),
do: new(Map.merge(normalize_opts(opts), %{kind: :card, card: value}))
def card(value, opts),
do: new(Map.merge(normalize_opts(opts), %{kind: :card, card: value}))
@doc "Builds a stream payload wrapper."
def stream(chunks, opts \\ []),
do: new(Map.merge(normalize_opts(opts), %{kind: :stream, stream: chunks}))
@doc "Builds a normalized outbound payload preserving post intent."
@spec to_payload(t()) :: PostPayload.t()
def to_payload(%__MODULE__{} = postable), do: postable |> Map.from_struct() |> PostPayload.new()
@doc "Flattens postable payload into adapter-safe text."
@spec to_text(t()) :: String.t()
def to_text(%__MODULE__{} = postable) do
payload = to_payload(postable)
PostPayload.display_text(payload) || ""
end
defp normalize_opts(opts) when is_list(opts), do: Map.new(opts)
defp normalize_opts(opts) when is_map(opts), do: opts
end