lib/open_rtb_ecto.ex

defmodule OpenRtbEcto do
  @moduledoc """
  Ecto schemas for OpenRTB 2.x & 3.0 according to the following specifications:

  [OpenRTB 2.0 Spec](https://www.iab.com/wp-content/uploads/2015/06/OpenRTB_API_Specification_Version2_0_FINAL.pdf)
  [OpenRTB 2.5 Spec](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf)
  [OpenRTB 2.6 Spec](https://iabtechlab.com/wp-content/uploads/2022/04/OpenRTB-2-6_FINAL.pdf)

  [OpenRTB 3.0 Spec](https://iabtechlab.com/wp-content/uploads/2017/09/OpenRTB-3.0-Draft-Framework-for-Public-Comment.pdf)
  """

  @typedoc "Any of the schemas defined in the library"
  @type open_rtb_schema :: term()

  @spec cast(open_rtb_schema(), map() | binary()) :: {:ok, struct()} | {:error, map()}
  def cast(schema, %{} = data) do
    with %{valid?: true} = changeset <- schema.changeset(struct(schema, %{}), data) do
      {:ok, Ecto.Changeset.apply_changes(changeset)}
    else
      %Ecto.Changeset{} = invalid_changeset ->
        {:error, format_invalid_changeset(invalid_changeset)}
    end
  end

  def cast(schema, json) when is_binary(json) do
    with {:ok, decoded} <- Jason.decode(json) do
      cast(schema, decoded)
    end
  end

  defp format_invalid_changeset(changeset) do
    Ecto.Changeset.traverse_errors(changeset, fn chg, atom, {msg, opts} ->
      input = Ecto.Changeset.get_field(chg, atom)
      msg = msg <> ", got #{inspect(input, charlists: :as_lists)}"

      Enum.reduce(opts, msg, fn
        {key, value}, acc ->
          String.replace(acc, "%{#{key}}", inspect(value))
      end)
    end)
  end
end