Peri

Peri is a schema validation library for Elixir, inspired by Clojure's Plumatic Schema. It focuses on validating raw maps and supports nested schemas and optional fields. With Peri, you can define schemas and validate data structures in a concise and readable manner.

Features

Installation

To use Peri in your project, add it to your dependencies in mix.exs:

def deps do
[
{:peri, "~> 0.1.0"}
]
end

Then, run mix deps.get to fetch the dependencies.

Usage

Defining Schemas

To define a schema, use the defschema macro. By default, all fields in the schema are optional unless specified as {:required, type}.

defmodule MySchemas do
import Peri
defschema :product, %{
id: {:required, :integer},
name: {:required, :string},
price: :float,
in_stock: :boolean
}
end

Validating Data

You can then use the schema to validate data:

product_data = %{id: 1, name: "Laptop", price: 999.99, in_stock: true}
case MySchemas.product(product_data) do
{:ok, valid_data} -> IO.puts("Data is valid!")
{:error, errors} -> IO.inspect(errors, label: "Validation errors")
end

Available Types

Peri supports the following types for schema definitions:

Examples

Simple Schema Validation

defmodule MySchemas do
import Peri
defschema :tag, :string
end
data = "valid!"
case MySchemas.order(data) do
{:ok, ^data} -> IO.puts("Data is valid!")
{:error, errors} -> IO.inspect(errors, label: "Validation errors")
end
invalid = 42
case MySchemas.order(data) do
{:ok, valid} -> IO.puts("Data is valid!")
{:error, errors} -> IO.inspect(errors, label: "Validation errors")
end

Simple Map Schema Validation

defmodule MySchemas do
import Peri
defschema :order, %{
order_id: {:required, :integer},
customer_name: {:required, :string},
total_amount: :float
}
end
order_data = %{order_id: 123, customer_name: "Alice", total_amount: 59.99}
case MySchemas.order(order_data) do
{:ok, valid_data} -> IO.puts("Data is valid!")
{:error, errors} -> IO.inspect(errors, label: "Validation errors")
end
invalid_order_data = %{order_id: 123}
case MySchemas.order(invalid_order_data) do
{:ok, valid_data} -> IO.puts("Data is valid!")
{:error, errors} -> IO.inspect(errors, label: "Validation errors")
end

Raw Schema Validation

You don't even need to use the defschema/2 macro, jsut define the schema as you want and pass them with data to Peri/validate/2 (actually is exactly that the macro does: creates a local function with the name of the schema and pass along the schema, receiving data as extra argument.)

defmodule MySchemas do
@raw_user_schema %{age: :integer, name: :string}
def create_user(data) do
with {:ok, data} <- Peri.validate(@raw_user_schema, data) do
# rest of function ....
end
end
end

Nested Schema Validation

defmodule MySchemas do
import Peri
defschema :user_profile, %{
username: {:required, :string},
email: {:required, :string},
details: %{
age: :integer,
bio: :string
}
}
end
profile_data = %{
username: "bob_smith",
email: "bob@example.com",
details: %{age: 30, bio: "Software developer"}
}
case MySchemas.user_profile(profile_data) do
{:ok, valid_data} -> IO.puts("Data is valid!")
{:error, errors} -> IO.inspect(errors, label: "Validation errors")
end
invalid_profile_data = %{
username: "bob_smith",
email: "bob@example.com",
details: %{age: "thirty"}
}
case MySchemas.user_profile(invalid_profile_data) do
{:ok, valid_data} -> IO.puts("Data is valid!")
{:error, errors} -> IO.inspect(errors, label: "Validation errors")
end

Composable and Recursive Schemas

Composable Schemas

Composable schemas allow you to define reusable schema components and combine them to create more complex schemas. As Peri's schemas are just functions (and under the hoods simple elixir data), you can pass schemas around to another schemas.

defmodule MySchemas do
import Peri
# Define reusable schemas
defschema :string_list, {:list, :string}
defschema :user_info, %{username: :string, email: {:required, {:custom, &string_list/1}}}
defschema :user_data, %{user_id: :integer, info: {:custom, &user_info/1}}
# Example usage
def example_usage do
data = ["hello", "world"]
case string_list(data) do
{:ok, valid_data} -> IO.puts("StringList is valid!")
{:error, errors} -> IO.inspect(errors, label: "Validation errors")
end
user_data_example = %{user_id: 1, info: %{username: "john_doe", email: "john@example.com"}}
case user_data(user_data_example) do
{:ok, valid_data} -> IO.puts("UserData is valid!")
{:error, errors} -> IO.inspect(errors, label: "Validation errors")
end
end
end

Another way is to define those composable schemas as raw data maps (or even other data types)

defmodule MySchemas do
import Peri
# Define reusable schemas
@string_list {:list, :string}
@user_info %{username: :string, email: {:required, @string_list}}
defschema :user_data, %{user_id: :integer, info: @user_info}
# Example usage
def example_usage do
data = ["hello", "world"]
case string_list(data) do
{:ok, valid_data} -> IO.puts("StringList is valid!")
{:error, errors} -> IO.inspect(errors, label: "Validation errors")
end
user_data_example = %{user_id: 1, info: %{username: "john_doe", email: "john@example.com"}}
case user_data(user_data_example) do
{:ok, valid_data} -> IO.puts("UserData is valid!")
{:error, errors} -> IO.inspect(errors, label: "Validation errors")
end
end
end

Recursive Schemas

Recursive schemas allow you to define schemas that reference themselves, enabling the validation of nested and hierarchical data structures.

defmodule MySchemas do
import Peri
# Define a recursive schema
defschema :category, %{
id: :integer,
name: :string,
subcategories: {:list, {:custom, &category/1}}
}
# Example usage
def example_usage do
category_data = %{
id: 1,
name: "Electronics",
subcategories: [
%{id: 2, name: "Computers", subcategories: []},
%{id: 3, name: "Phones", subcategories: [%{id: 4, name: "Smartphones", subcategories: []}]}
]
}
case category(category_data) do
{:ok, valid_data} -> IO.puts("Category is valid!")
{:error, errors} -> IO.inspect(errors, label: "Validation errors")
end
end
end

Comparison: Peri vs. Ecto

While both Peri and Ecto provide mechanisms for working with schemas in Elixir, they serve different purposes and are used in different contexts.

Peri

Ecto

Summary

By understanding the different strengths and use cases of Peri and Ecto, you can choose the right tool for your specific needs, ensuring efficient and effective data handling in your Elixir applications.

Why "Peri"?

The name "Peri" is derived from the prefix "peri-" which means "around" or "surrounding". This reflects the library's purpose of providing a protective layer around your data structures, ensuring they conform to specified schemas. Just as a perimeter protects and defines boundaries, Peri ensures that your data is validated and well-defined according to your schemas.

Contributing

We welcome contributions to Peri! If you have suggestions for improvements or find bugs, please open an issue or submit a pull request on GitHub.

License

Peri is released under the MIT License. See the LICENSE file for more details.


Peri makes it easy to define and validate data structures in Elixir, providing a powerful tool for ensuring data integrity in your applications. Start using Peri today and enjoy simple, reliable schema validation!