use Vtc.Ecto.Postgres.Utils defpgmodule Vtc.Ecto.Postgres.PgFramestamp do @moduledoc """ Defines a composite type for storing rational values as a [PgRational](`Vtc.Ecto.Postgres.PgRational`) real-world playbck seconds, [PgFramerate](`Vtc.Ecto.Postgres.PgFramerate`) pair. These values are cast to [Framestamp](`Vtc.Framestamp`) structs for use in application code. The composite types is defined as follows: ```sql CREATE TYPE framestamp as ( seconds rational, rate framerate ) ``` ```sql SELECT ((18018, 5), ((24000, 1001), '{non_drop}'))::framestamp ``` ## Field migrations You can create `framerate` fields during a migration like so: ```elixir alias Vtc.Framestamp create table("events") do add(:in, Framestamp.type()) add(:out, Framestamp.type()) end ``` [Framestamp](`Vtc.Framestamp`) re-exports the `Ecto.Type` implementation of this module, and can be used any place this module would be used. ## Schema fields Then in your schema module: ```elixir defmodule MyApp.Event do @moduledoc false use Ecto.Schema alias Vtc.Framestamp @type t() :: %__MODULE__{ in: Framestamp.t(), out: Framestamp.t() } schema "events" do field(:in, Framestamp) field(:out, Framestamp) end ``` ## Changesets With the above setup, changesets should just work: ```elixir def changeset(schema, attrs) do schema |> Changeset.cast(attrs, [:in, :out]) |> Changeset.validate_required([:in, :out]) end ``` Framerate values can be cast from the following values in changesets: - [Framestamp](`Vtc.Framestamp`) structs. - Maps with the following format: ```json { "smpte_timecode": "01:00:00:00", "rate": { "playback": [24000, 1001], "ntsc": "non_drop" } } ``` Where `smpte_timecode` is properly formatted SMPTE timecode string and `playback` is a map value supported by [PgFramerate](`Vtc.Ecto.Postgres.PgFramerate`) changeset casts. ## Fragments Framestamp values must be explicitly cast using [type/2](https://hexdocs.pm/ecto/Ecto.Query.html#module-interpolation-and-casting): ```elixir framestamp = Framestamp.with_frames!("01:00:00:00", Rates.f23_98()) query = Query.from(f in fragment("SELECT ? as r", type(^framestamp, Framestamp)), select: f.r) ``` """ use Ecto.Type alias Ecto.Changeset alias Vtc.Ecto.Postgres.PgFramerate alias Vtc.Ecto.Postgres.PgRational alias Vtc.Framerate alias Vtc.Framestamp alias Vtc.Source.Frames.SMPTETimecodeStr @doc section: :ecto_migrations @doc """ The database type for [PgFramerate](`Vtc.Ecto.Postgres.PgFramerate`). Can be used in migrations as the fields type. """ @impl Ecto.Type def type, do: :framestamp @typedoc """ Type of the raw composite value that will be sent to / received from the database. """ @type db_record() :: {PgRational.db_record(), PgFramerate.db_record()} # Handles casting PgRational fields in `Ecto.Changeset`s. @doc false @impl Ecto.Type @spec cast(Framestamp.t() | %{String.t() => any()} | %{atom() => any()}) :: {:ok, Framerate.t()} | :error def cast(%Framestamp{} = framestamp), do: {:ok, framestamp} def cast(json) when is_map(json) do schema = %{ smpte_timecode: :string, rate: Framerate } changeset = {%{}, schema} |> Changeset.cast(json, [:smpte_timecode, :rate]) |> Changeset.validate_required([:smpte_timecode, :rate]) with {:ok, %{smpte_timecode: timecode_str, rate: rate}} <- Changeset.apply_action(changeset, :loaded), {:ok, _} = result <- Framestamp.with_frames(%SMPTETimecodeStr{in: timecode_str}, rate) do result else _ -> :error end end def cast(_), do: :error # Handles converting database records into Ratio structs to be used by the # application. @doc false @impl Ecto.Type @spec load(db_record()) :: {:ok, Framestamp.t()} | :error def load({seconds, rate}) do with {:ok, seconds} <- PgRational.load(seconds), {:ok, framerate} <- PgFramerate.load(rate), {:ok, _} = result <- Framestamp.with_seconds(seconds, framerate, round: :off) do result else _ -> :error end end def load(_), do: :error # Handles converting Ratio structs into database records. @doc false @impl Ecto.Type @spec dump(Framestamp.t()) :: {:ok, db_record()} | :error def dump(%Framestamp{} = framestamp) do with {:ok, seconds} <- PgRational.dump(framestamp.seconds), {:ok, framerate} <- PgFramerate.dump(framestamp.rate) do {:ok, {seconds, framerate}} end end def dump(_), do: :error end