Skip to main content

lib/jido/chat/file_upload.ex

defmodule Jido.Chat.FileUpload do
  @moduledoc """
  Canonical outbound file upload request used by posting and upload helpers.
  """

  alias Jido.Chat.{Attachment, Wire}

  @schema Zoi.struct(
            __MODULE__,
            %{
              kind: Zoi.enum([:image, :audio, :video, :file]) |> Zoi.default(:file),
              url: Zoi.string() |> Zoi.nullish(),
              path: Zoi.string() |> Zoi.nullish(),
              data: Zoi.string() |> Zoi.nullish(),
              media_type: Zoi.string() |> Zoi.nullish(),
              filename: Zoi.string() |> Zoi.nullish(),
              size_bytes: Zoi.integer() |> Zoi.nullish(),
              width: Zoi.integer() |> Zoi.nullish(),
              height: Zoi.integer() |> Zoi.nullish(),
              duration: Zoi.integer() |> Zoi.nullish(),
              metadata: Zoi.map() |> Zoi.default(%{})
            },
            coerce: true
          )

  @type t :: unquote(Zoi.type_spec(@schema))
  @type input :: t() | Attachment.input()

  @enforce_keys Zoi.Struct.enforce_keys(@schema)
  defstruct Zoi.Struct.struct_fields(@schema)

  @doc "Returns the Zoi schema for FileUpload."
  def schema, do: @schema

  @doc "Creates a file upload request from normalized map input."
  def new(%__MODULE__{} = file_upload), do: file_upload

  def new(attrs) when is_map(attrs),
    do: attrs |> normalize_map() |> then(&Jido.Chat.Schema.parse!(__MODULE__, @schema, &1))

  @doc "Normalizes supported upload inputs into a canonical file upload struct."
  @spec normalize(input()) :: t()
  def normalize(%__MODULE__{} = file_upload), do: file_upload

  def normalize(%Attachment{} = attachment), do: from_attachment(attachment)
  def normalize(input), do: input |> Attachment.normalize() |> from_attachment()

  @doc "Normalizes a list of file upload inputs."
  @spec normalize_many([input()]) :: [t()]
  def normalize_many(files) when is_list(files), do: Enum.map(files, &normalize/1)

  @doc "Serializes a file upload into a plain map with type marker."
  @spec to_map(t()) :: map()
  def to_map(%__MODULE__{} = file_upload) do
    file_upload
    |> Map.from_struct()
    |> Wire.to_plain()
    |> Map.put("__type__", "file_upload")
  end

  @doc "Builds a file upload from serialized map data."
  @spec from_map(map()) :: t()
  def from_map(map) when is_map(map), do: map |> Map.drop(["__type__", :__type__]) |> new()

  defp from_attachment(%Attachment{} = attachment) do
    attachment
    |> Map.from_struct()
    |> new()
  end

  defp normalize_map(attrs) do
    attrs
    |> Map.drop(["__type__", :__type__])
    |> Enum.into(%{}, fn
      {:type, value} -> {:kind, value}
      {"type", value} -> {:kind, value}
      pair -> pair
    end)
  end
end