TypedEctoSchema provides a DSL on top of Ecto.Schema to define schemas with typespecs without all
the boilerplate code.
For example, if you want to add type information about your Ecto.Schema, you normally do something
like this:
defmodule Person do
use Ecto.Schema
@enforce_keys [:name]
schema "people" do
field(:name, :string)
field(:age, :integer)
field(:happy, :boolean, default: true)
field(:phone, :string)
belongs_to(:company, Company)
timestamps(type: :naive_datetime_usec)
end
@type t() :: %__MODULE__{
__meta__: Ecto.Schema.Metadata.t(),
id: integer() | nil,
name: String.t(),
age: non_neg_integer() | nil,
happy: boolean(),
phone: String.t() | nil,
company_id: integer() | nil,
company: Company.t() | Ecto.Association.NotLoaded.t() | nil,
inserted_at: NaiveDateTime.t() | nil,
updated_at: NaiveDateTime.t() | nil
}
endWith typed_ecto_schema you can just do:
defmodule Person do
use TypedEctoSchema
typed_schema "people" do
field(:name, :string, enforce: true, null: false)
field(:age, :integer) :: non_neg_integer() | nil
field(:happy, :boolean, default: true, null: false)
field(:phone, :string)
belongs_to(:company, Company)
timestamps(type: :naive_datetime_usec)
end
endNote that the timestamps are nullable by default (a struct that was not inserted yet has nil
timestamps). You can opt out with timestamps(null: false).
Install it, add to your deps:
{:typed_ecto_schema, "~> 0.4.4", runtime: false}And change your use Ecto.Schema for use TypedEctoSchema and change the calls to schema for
typed_schema and embedded_schema to typed_embedded_schema.
The extra :null and :enforce options also work on the @primary_key attribute, so you can
have a non-nullable (and/or enforced) primary key type:
@primary_key {:id, :binary_id, autogenerate: true, null: false}Check the online documentation for further details.
polymorphic_embed's
polymorphic_embeds_one/2 and polymorphic_embeds_many/2 are supported inside
typed_schema blocks behind a compile-time flag, disabled by default:
# config/config.exs
config :typed_ecto_schema, polymorphic_embed: trueThe calls are recognized purely by name (so polymorphic_embed never becomes a dependency
of this library) — the flag exists because another library could define same-named macros
with different behavior, so only enable it if you use polymorphic_embed. With the flag
disabled the calls behave exactly as before. The flag must live in compile-time config
(config.exs, not runtime.exs) and requires Elixir 1.14+. This integration is
experimental and may change in a future release.
Once enabled, the typespec is inferred as the union of the modules listed in the
:types option:
defmodule Reminder do
use TypedEctoSchema
import PolymorphicEmbed
typed_schema "reminders" do
polymorphic_embeds_one(:channel,
types: [sms: SMS, email: Email],
on_replace: :update
)
end
endThis generates channel: (SMS.t() | Email.t()) | nil, while polymorphic_embeds_many
generates a list of the union instead. The :: type override and the :null and :enforce
options work just like they do for field/3 (and are stripped before the real
polymorphic_embed macro runs):
# SMS.t() | Email.t() (without `| nil`), and :channel is added to @enforce_keys
polymorphic_embeds_one(:channel,
types: [sms: SMS, email: Email],
on_replace: :update,
null: false,
enforce: true
)As with embeds_many, :null has no effect on polymorphic_embeds_many, since it is
always initialized to an empty list. When the :types option cannot be resolved at
compile time (for example, when it is a module attribute), the type falls back to any().
Since polymorphic_embed is not a dependency of this library, you still need to add it to
your own deps and import it in your schema modules yourself.
This project started as a fork of the awesome typed_struct.
That being said, I'd like to give some special thanks to
- Jean-Philippe Cugnet for laying the ground for this work.
- Carlos Brito Lage for helping me with planning and ideas about the DSL.