Parsing and Validating Data in Elixir
Tags: webdev, elixir, parsing, programming
Published: 2024-06-18 22:40:53

In the enchanting world of Elixir programming, data validation is a quest every developer embarks on. It's a journey through the land of schemas, types, and constraints, ensuring data integrity and correctness. Today, we'll explore four powerful artifacts: Ecto, Norm, Drops, and Peri. Each of these tools offers unique powers for taming your data. We'll delve into their strengths, use cases, and compare them to help you choose the right one for your quest.

## The Quest: Parse, Don't Validate

Before we embark on our journey, let's discuss a guiding principle in functional programming: **Parse, Don't Validate**. This pattern emphasizes transforming data into a well-defined structure as early as possible. By doing so, you avoid scattered, ad-hoc validation throughout your codebase, leading to clearer, more maintainable code. It's like casting a spell to organize the chaos of raw data into a neat, structured form.

## The Artifacts

### 1. Ecto

Ecto is a robust toolkit primarily designed for interacting with databases. However, it also offers powerful capabilities for embedded schemas and schemaless changesets, making it versatile for data validation.

#### Embedded Schemas

Ecto allows defining schemas that don't map to a database table, ideal for validating nested data structures.

```elixir
defmodule User do
  use Ecto.Schema

  embedded_schema do
    field :name, :string
    field :email, :string
  end
end

def changeset(data) do
  %User{}
  |> Ecto.Changeset.cast(data, [:name, :email])
  |> Ecto.Changeset.validate_required([:name, :email])
end
```

#### Schemaless Changesets

For dynamic data, Ecto provides schemaless changesets, offering flexibility at the cost of increased complexity.

```elixir
def changeset(data) do
  Ecto.Changeset.cast({%{}, %{name: :string, email: :string}}, data, [:name, :email])
  |> Ecto.Changeset.validate_required([:name, :email])
end
```

### 2. Norm

Norm focuses on defining and conforming to data structures with custom predicates, offering a clean syntax and powerful validation.

```elixir
defmodule User do
  import Norm

  defschema do
    schema(%{
      name: spec(is_binary()),
      age: spec(is_integer() and &(&1 > 18))
    })
  end
end

Norm.conform(%{name: "Jane", age: 25}, User.schema())
# => {:ok, %{name: "Jane", age: 25}}
```

### 3. Drops

Drops is a newer library that provides a rich set of tools for defining and validating schemas, leveraging Elixir's type system.

```elixir
defmodule UserContract do
  use Drops.Contract

  schema do
    %{
      required(:name) => string(:filled?),
      required(:age) => integer(gt?: 18)
    }
  end
end

UserContract.conform(%{name: "Jane", age: 21})
# => {:ok, %{name: "Jane", age: 21}}
```

### 4. Peri

Peri is inspired by Clojure's Plumatic Schema, focusing on validating raw maps with nested schemas and optional fields. It's designed to be powerful yet simple, embracing the "Parse, Don't Validate" pattern.

```elixir
defmodule MySchemas do
  import Peri

  defschema :user, %{
    name: :string,
    age: :integer,
    email: {:required, :string},
    role: {:enum, [:admin, :user]}
  }

  defschema :profile, %{
    user: {:custom, &MySchemas.user/1},
    bio: :string
  }
end

MySchemas.user(%{name: "John", age: 30, email: "john@example.com", role: :admin})
# => {:ok, %{name: "John", age: 30, email: "john@example.com", role: :admin}}

MySchemas.user(%{name: "John", age: "thirty", email: "john@example.com"})
# => {:error, [%Peri.Error{path: [:age], message: "expected integer received \"thirty\""}]}
```

### Conditional and Composable Types in Peri

Peri shines with its support for conditional and composable types, making it a powerful tool for complex validation scenarios.

```elixir
defmodule AdvancedSchemas do
  import Peri

  defschema :user, %{
    name: :string,
    age: {:cond, &(&1 >= 18), :integer, :nil},
    email: {:either, {:string, :nil}},
    preferences: {:list, {:oneof, [:string, :atom]}}
  }
end

AdvancedSchemas.user(%{name: "Alice", age: 25, email: nil, preferences: ["coding", :reading]})
# => {:ok, %{name: "Alice", age: 25, email: nil, preferences: ["coding", :reading]}}

AdvancedSchemas.user(%{name: "Bob", age: 17})
# => {:ok, %{name: "Bob", age: 17, email: nil, preferences: nil}}
```

## Conclusion

Each of these tools offers unique advantages and caters to different needs:

- **Ecto** is great for data associated with databases but can handle schemaless and embedded data structures too.
- **Norm** provides a clean and powerful way to define and validate data structures.
- **Drops** leverages Elixir's type system and offers rich schema definitions and validations.
- **Peri** emphasizes simplicity and power, supporting complex types and conditional validations.

By understanding the strengths and weaknesses of each, you can choose the right tool for your data validation needs in Elixir. Happy coding, fellow sorcerers of Elixiria!

### References

- [Ecto Documentation](https://hexdocs.pm/ecto/Ecto.html)
- [Norm Documentation](https://hexdocs.pm/norm/Norm.html)
- [Drops Documentation](https://hexdocs.pm/drops/Drops.html)
- [Peri Documentation](https://hexdocs.pm/peri/Peri.html)

Feel free to dive into the source code and contribute to these projects to make Elixiria an even more magical place!