Ecto for validation only

In rails you can create a model who does not inherit from ActiveRecord but rather simply imports a few methods for the purpose of validation only. ( Think contact form that is not persisted. )

In that case you can
include ActiveModel::Model

So naturally I was thinking I want to try to do that same with ecto.

At the moment I have this contact form and I wanted your thoughts about a better way to do this.

Controller.

defmodule PolymorphicProductionsWeb.ContactController do
  use PolymorphicProductionsWeb, :controller
  alias PolymorphicProductions.{Messages, Messages.Contact}

  ...

  def create(conn, %{"contact" => contact_params}) do
    case Messages.create_contact(contact_params) do
      {:ok, contact} ->
        conn
        |> put_flash(:info, "Your messages was successfully sent.")
        |> redirect(to: Routes.page_path(conn, :index))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html",
          layout: {PolymorphicProductionsWeb.LayoutView, "full-header.html"},
          changeset: changeset
        )
    end
  end
end

Context:

defmodule PolymorphicProductions.Messages do
  alias PolymorphicProductions.Messages.Contact

  @doc """
  Creates a contact message and sends it.
  """
  def create_contact(attrs \\ %{}) do
    case Contact.changeset(%Contact{}, attrs) do
      %{valid?: true} = contact ->
        {:ok, contact}

      changeset ->
        Ecto.Changeset.apply_action(changeset, :insert)
    end
  end

  def change_contact(%Contact{} = contact) do
    Contact.changeset(contact, %{})
  end
end

Schema:

defmodule PolymorphicProductions.Messages.Contact do
  use Ecto.Schema

  import Ecto.Changeset

  schema "contacts" do
    field(:name, :string, virtual: true)
    field(:email, :string, virtual: true)
    field(:subject, :string, virtual: true)
    field(:message, :string, virtual: true)
  end

  @doc false
  def changeset(contact, attrs) do
    contact
    |> cast(attrs, [:name, :email, :subject, :message])
    |> validate_required([:name, :email, :subject, :message])
  end
end

At the moment I have a schema who does not really have a table of contacts and I also do this hack to get the phoenix form helpers to render.

Ecto.Changeset.apply_action(changeset, :insert)
1 Like

Ecto supports validation of maps as well, as long as the types are passed in.

from Ecto.Changeset — Ecto v3.11.1

The given data may be either a changeset, a schema struct or a {data, types} tuple. The second argument is a map of params that are cast according to the type information from data . params is a map with string keys or a map with atom keys containing potentially invalid data.

2 Likes

trying to understand the structor of the {data, types}

Would that be like.

{
  %{some_string: "foo", some_int: 0},
  %{some_string: "string", some_int: "integer"}
}

oh derp, I guess I just needed to read a little more, awesome solution thank you!

Just use embedded_schema instead of schema "table" and you’re fine and you no longer need to mark any field as virtual. Also using apply_action is not a hack, but what you’re supposed to be doing in such a case.

3 Likes

Thanks for reassuring me about apply_action I really was not sure if I should be doing that.

Whats your thought about this

defmodule PolymorphicProductions.Messages.Contact do
  defstruct name: nil, email: nil, subject: nil, message: nil

  import Ecto.Changeset

  @types %{name: :string, email: :string, subject: :string, message: :string}

  @doc false
  def changeset(contact, attrs) do
    {contact, @types}
    |> cast(attrs, [:name, :email, :subject, :message])
    |> validate_required([:name, :email, :subject, :message])
  end
end

vs

defmodule PolymorphicProductions.Messages.Contact do
  use Ecto.Schema

  import Ecto.Changeset

  embedded_schema do
    field(:name, :string)
    field(:email, :string)
    field(:subject, :string)
    field(:message, :string)
  end

  @doc false
  def changeset(contact, attrs) do
    contact
    |> cast(attrs, [:name, :email, :subject, :message])
    |> validate_required([:name, :email, :subject, :message])
  end
end

Edit:

Ok so embedded schema are expected for this uses case after all. Nice.
On the other hand, embedded_schema/1 is used for defining schemas that are embedded in other schemas or only exist in-memory.

1 Like