Data Division

A library that generates data-holding structures with validation, error maps, and compatibility with Phoenix form_for.

Let's Get Started

  1. Add the dependency:

    def deps() do
    [
    :data_division, ">= 0.0.0",
    . . .
    ]
  2. Define a record structure:

    defmodule Planet do
    use DD
    defrecord do
    string :name, min: 4
    float :mass
    bool :habitable, default: false
    int :moon_count
    end
    end
  3. Create and populate structures based on this record:

    neptune = Planet.new_record(
    name: "Neptune",
    mass: 1.024e26,
    moon_count: 14)
  4. They found another moon:

    new_neptune = neptune |> Planet.update(moon_count: 15)
  5. Is the record valid? If not, what are the errors?

    if !Planet.valid?(neptune) do
    for { field, error_msg } <- neptune.errors do
    IO.puts "#{field}: #{error_msg}"
    end
    end
  6. Let's use it in Phoenix:

    controller action:

    render conn, "edit.html", planet: neptune

    template:

    <%= form_for @planet, .....

defrecord

defrecord is a bit like Ecto's schema. It defines a struct that contains a place for data (like the planet information above) and a place for metadata on field types, options, and so on.

It should be used in a module, just like defstruct:

defmodule Planet do
use DD
defrecord do
string :name, min: 4
float :mass, min: 0.0
bool :habitable, default: false
int :moon_count
end
end

This code defines a record called Planet with 4 fields. Each field definition starts with the field type, followed by the field name (an atom). The rest depends to some extent on the type of the field, although all fields support default values.

In this example, the name field has a validation: it must be at least 4 characters long. Similarly the mass field has a validation: it cannot be less that zero. Note that although the option is named the same in both cases min:) its interpretation depends on the field type: for strings it is the length, for floats the value.

A list of the available types and their options is below.

Using a Record

You create new instances of a record using Name.new_record(values). The values you pass in can be a keyword list, map, or struct. If values is a struct of type Ecto.Changeset, then values and errors are copied directly from it into the record.

new_record returns a structure containing three entries:

So, we could do something like:

neptune = Planet.new_record(
name: "Neptune",
moon_count: 14)
IO.inspect neptune.errors #=> %{ mass: "must be present" }
neptune = Planet.update(neptune, mass: 1.024e26
IO.inspect neptune.errors #=> %{ }
IO.inspect neptune.valid? #=> true

Built-in Types

Custom Validations

You can define your own field validators. Each is a function that takes a value and returns either

nil

if the value is valid, or

{ msg_with_placeholders, p1: v1, … }

if it is invalid. In the latter case the first field in the returned tuple is a string containing optional placeholders. The values that are to be substituted for each placeholder are given in the subsequent keyword list:

{ "%{value} must have exactly %{n} factors", value: 30, n: 2 }

Add a custom validator to a field using the validate_with: option. This takes either a single validator or a list of validators.

If a validator is the name of a module, then the field is validted by calling the function validate/1 in that module.

If a validator is a function, then it is called withe the value to validate.

For example:

# This is a validation module
defmodule EvenValidator do
require Integer
def validate(value) when Integer.is_even(value), do: nil
def validate(_), do: { "must be even..", [] }
end
# and this module contains validation functions
defmodule Validations do
require Integer
def is_even(value) when Integer.is_even(value), do: nil
def is_even(_), do: { "must be even!!", [] }
end
defmodule A do
use DD
defrecord do
int(:even1, default: 2, validate_with: EvenValidator)
int(:even2, default: 2, validate_with: &Validations.is_even/1)
end
end

Adding Your Own Types

A type is simply an Elixir module that:

  1. is named DD.Type.YourType

  2. uses the behaviour DD.Type.Behaviour

  3. implements the handful of functions required by that behaviour.

If this module is loaded into your project, then the type becomes available in defrecord as if it was a function named using the lowercase form of the last part of the module name. So, in this example, you could have

defrecord do string(:name) your_type(:orbit_parameters) end

See the module doc for Data.Type for details.