Search code examples
elixirphoenix-frameworkecto

Can a default new function be created for all Ecto models using a macro?


I would like all of my models to have access to a new convenience function that returns a custom changeset.

Something like:

def new(attrs) do
  changeset(%__MODULE__{}, attrs)
end

That way, when I need changeset validation, I could call:

Project.Model.new(%{param1: "param1"})

Instead of:

Project.Model.changeset(%Model{}, %{param1: "param1"})

The problem I run into is that when I implement the following macro:

defmodule Project.Model do
  defmacro __using__(_) do
    quote do
      use Ecto.Schema
      import Ecto.Changeset
      def new(attrs) do
        changeset(%__MODULE__{}, attrs)
      end
    end
  end
end

... it doesn't work, because Ecto's schema "model" do ... end needs to be compiled before my use Project.Model statement otherwise I'll get an error which basically says my module does not have a struct defined.

I could limit the macro simply to the new function and place it in the middle of the module that uses it but that seems confusing.

Any ideas?

By request, full code below:

Once again, the purpose of all this, is to have models that use Project.Model gain a convenience function new that accepts attributes and puts them inside a changeset so that they will be validated before database insertion.

The error I get, as some have alluded to, is that schema must be expanded before I can use __MODULE__ in my macro because the struct will not have yet been defined.

project/user.ex

defmodule Project.User do
  use Project.Model

  schema "users" do
    field :email, :string
    field :first_name, :string
    field :last_name, :string

    timestamps()
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :first_name, :last_name])
    |> validate_required([:email, :first_name, :last_name])
  end
end

project/model.ex

defmodule Project.Model do
  defmacro __using__(_) do
    quote do
      use Ecto.Schema
      import Ecto.Changeset
      def new(attrs) do
        changeset(%__MODULE__{}, attrs)
      end
    end
  end
end

Solution

  • To use a struct before it's defined, you can try this trick:

      def new(attrs) do
        changeset(struct(__MODULE__), attrs)
      end
    

    changeset is something you should define per model (and might have a different shape) so I'm not sure it's a good idea to use it inside new in a general manner.