typed_ecto_schema v0.1.0 TypedEctoSchema View Source

TypedEctoSchema provides a DSL on top of Ecto.Schema to define schemas with typespecs without all the boilerplate code.

Rationale

Normally, when defining an Ecto.Schema you probably want to define:

  • the schema itself
  • the list of enforced keys (which helps reducing problems)
  • its associated type (Ecto.Schema doesn't define it for you)

It ends up in 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(),
          updated_at: NaiveDateTime.t()
        }
end

This is problematic for a a lot of reasons, summing up:

  • A lot of repetition. Field names appear in 3 different places, so in order to understand one field, a reader needs to go up and down the code to get that.
  • Ecto has some "hidden" fields that are added behind the scenes to the struct, such as the primary key id, the foreignkey company_id, the timestamps and the `_meta` field for schemas. Knowing all those rules can be hard to remember and would probably be easily forgotten when changing the schema. Also Ecto has strange types for associations and meta that need to be remembered.

All of this makes this process extremely repetitive and error prone. Sometimes, you want to enforce factory functions to control defaults in a better way, you would probably add all fields to @enforce_keys. This would make the @enforce_keys big and repetitive, once again.

This module aims to help with that, by providing some syntax sugar that allow you to define this in a more compact way.

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
end

This is way simpler and less error prone. There is a lot going under the hoods here.

Extra Options

All ecto macros are called under the hood with the options you pass, with exception of a few added options:

  • :null - when true, adds a | nil to the typespec. Default is true. Has no effect on has_one/3 because it can always be nil. On belongs_to/3 only add | nil to the underlying foreign key.
  • :enforce - when true adds the field to the @enforce_keys. Default is false

Schema Options

When calling typed_schema/3 or typed_embedded_schema/2 you can pass some options, as defined:

  • :null - Set the default :null field option, which normally is true. Note that it is still can be overwritten by passing :null to the field itself. Also, embeds_many and has_many can never be null, because they are always initialized to empty string, so they never receive the | nil on the typespec. In addition to that, has_one/3 and belongs_to/3 always receive | nil because the related schema may be deleted from the repo so it is safe to always assume they can be nil.
  • :enforce - When true, enforces all fields unless they explicitly set enforce: false or defines a default (default: value), since it makes no sense to have a default value for an enforced field.
  • :opaque - When true makes the generated type t be an opaque type.

Type Inference

TypedEctoSchema does it's best job to guess the typespec for the field. It does so by following the elxir types as defined in Ecto.Schema documentation. For custom Ecto.Type and related schemas (embedded and associations), which are always a module, it assumes the schemas has a type t/0 defined, so for a schema called MySchema, it will assume the type is MySchema.t/0, which is also, the default type generated by this library.

Overriding the Typespec for a field

If for somereason you want to narrow the type or our type inference is incorrect, we allow a way to override the typespec. You do this by using the :: operator, the same you use when defining typespecs.

So, for example, instead of

field(:my_int, :integer)

Which would generate a integer() | nil typespec, you can:

field(:my_int, :integer) :: non_neg_integer() | nil

And then have a non_neg_integer() type for it.

Non explicit generated fields

Ecto generates some fields for you in a lot of cases, they are:

  • For primary keys
  • When using a belongs_to/3
  • When calling timestamps/1

The __meta__ typespec is automatically generated and cannot be overriden. That is because there is no point on overriding it.

Primary Keys

Primary keys are generated by default and can be customized by the @primary_key module attribute, just as defined by Ecto. We handle @primary_key the same way we handle field, so you can pass the same field options to it.

However, if you want to customize the type, you need to set @primary_key false and define a field with a primary_key: true.

Belongs To

Belongs to generate an underlying foreign key that is dependent on a few Ecto options, as defined on Ecto.Schema documentation.

The options we have interest are :foreign_key, :define_field and :type

When you do :null it will add | nil to the generated foreign_key.

When you do :enforce it will enforce the association field instead. If you want to :enforce the foreign key to be set, you should probably pass define_field: false and define the foreign_key by hand by setting another field/3, the same way as described by Ecto's doc.

Timestamps

In case of the timestamps, we currently don't allow overriding the type using the :: operator. That being said, however, we define the type of the fields using the :type option (as defined by Ecto doc)

Link to this section Summary

Link to this section Functions

Link to this macro

typed_embedded_schema(opts \\ [], list)

View Source (macro)

Replaces Ecto.Schema.embedded_schema/1

Link to this macro

typed_schema(table_name, opts \\ [], list)

View Source (macro)

Replaces Ecto.Schema.schema/2