How to extend field from an Ecto schema with meta information?

I’d like to extend my Ecto schemas with meta information that I can use to compile to a JSON Schema. I’d like to do be able to do something like this:

schema "users" do
  field :email, :string, constraints: [min: 1, max: 10]
end

And then to be able to reflect om that with something like User.__schema__(:options, :email).

Is it possible to achieve this without replacing all field-calls by something else? I was thinking something along the lines of:

defmodule Blueprint do
  defmacro __using__(_) do
    quote do
      import Blueprint
      use Ecto.Schema
      Module.register_attribute(__MODULE__, :blueprint_fields, accumulate: true)
    end
  end

  defmacro blueprint(source, [do: block]) do
    
    # Do something to replace all calls in block to field by calls to blueprint_field
    
    quote do
      Ecto.Schema.schema(unquote(source), [do: unquote(block)])
      def __blueprint__(), do: @blueprint_fields |> Enum.reverse
    end
  end

  defmacro blueprint_field(name, type \\ :string, opts \\ []) do
    quote do
      Module.put_attribute(__MODULE__, :blueprint_fields, {unquote(name), unquote(type), unquote(opts)})
      Ecto.Schema.__field__(__MODULE__, unquote(name), unquote(type), unquote(opts))
    end
  end
end

defmodule User do
  use Blueprint
  
  blueprint "users" do
    field :email, :string, constraints: [min: 1, max: 10]
  end 
end

Is there a easy way to achieve the replacement of the field calls? (the part I left out at the start of the blueprint macro)
And, maybe even more important, should I want this? Or is this going against the spirit of the language? And, is there another way to achieve what I want?

I’m still interested whether or not this is the easiest wat to accomplish this, but I already found myself that I can implement this using Macro.prewalk/2

block = Macro.prewalk(block, fn
  {:field, meta, args} -> {:blueprint_field, meta, args}
  node -> node
end)
1 Like