Ecto Custom Types, a practical case with enumerize (Rails gem)

We keep on track with the migration of our Rails application to Elixir at Acutario, and here is another article about our trip :-).

We use a lot the awesome Rails gem enumerize. I know that there is an enum type in ActiveRecord now, but I think that enumerize has very interesting functionality like an easy internationalization, forms integration or multiple values selection.

For example, we use it for a setting in with you can select a set of week days. In Rails, we have an enumerized array with the numbers of the week days in Ruby and multiple selection enabled.

WEEK_DAYS = [0,1,2,3,4,5,6].freeze
extend Enumerize
serialize :week_days, Array
enumerize :week_days, in: WEEK_DAYS, multiple: true

Ok, let’s go then for the Elixir part. While we are on transition between languages, we need to consume some data with Elixir in the same way that we do in Rails. With this particular case we have some interesting issues.

First of all, enumerize stores this data as an array of the selected days of the week serialized as a yaml string. Secondly, in Ruby (date.wday), the week starts on Sunday (0) and ends on Saturday (6) but in Erlang (:calendar.day_of_the_week(date)), the week starts on Monday (1) and ends on Sunday (7).

So if we have a setting with the weekend days, the raw data in the database is:

“ — -\\n- ‘0’\\n- ‘7’\\n”

We want to get an array of Erlang days in our Elixir code:


And do the opposite when we save it to the database.

Ecto.Type to the rescue

Ecto.Type is an Elixir behavior that lets you create a custom Ecto type to define how to read and write a field from and to the data storage. And it is the perfect solution for this scenario.

Lets go and see how to create a custom type. First, you have to create a module, add the Ecto.Type behavior and implement 4 function callbacks.

  • type: returns the native Ecto type of the field in the data storage, like :integer, :string, {:array, :string}…

In our example, we have the days stored in a yaml in our database, so we need a :string:

  • cast: transforms the input data into the right format for the data storage. This function is called when you cast a struct into a changeset or when you pass arguments to an Ecto.Query. For example, if you need to store an Ecto.Date with different formats from the web forms (like “dd/mm/yyyy”), you can create a cast function for each format and when you create the changeset with the params, it will contain the Ecto.Date:
Ecto.Changeset.cast(%Model{}, %{date: “01/01/2017”},~w(date))
#Ecto.Changeset<action: nil,
 changes: %{date: #Ecto.Date<2017–01–01>}, errors: [],
 data: #Model<>, valid?: true>

We want to use an integer array until we write the data into database, so we just need to ensure that we accept valid week days:

  • load: process raw data (the Ecto native type) when is read from the data storage and transform it to our custom type.

We have a yaml stored, so we need a function to parse the yaml, create an integer array and translate each value into Erlang week days (replace day 0 with 7). There are some yaml libraries in Erlang/Elixir like yamerl, but we always expect the same structure so we can save us a new dependency and do a simple split.

  • dump: validate the input data (casted data in the data struct) and transform it into the Ecto native type before is written to the data storage.

So finally, we have to do the opposite process, translate Erlang week days into Rails week days and compose the yaml that will be stored in the database.

Now we can use the type in the Ecto schema declaration:

schema “settings” do
 field :week_days, WeekDays

And this is it, with this new type we can read and write the week_days field, manage it like a normal integer array in the Elixir code and keep the consistence with the Rails application.

I hope this can be helpful for a similar case or just for a better understanding of Ecto.Type.

See you in another post! 😉

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.