View Source Peri (peri v0.2.6)

Peri is a schema validation library for Elixir, inspired by Clojure's Plumatic Schema. It provides a flexible and powerful way to define and validate data structures using schemas. The library supports nested schemas, optional fields, custom validation functions, and various type constraints.

Key Features

  • Simple and Nested Schemas: Define schemas that can handle complex, nested data structures.
  • Optional and Required Fields: Specify fields as optional or required with type constraints.
  • Custom Validation Functions: Use custom functions to validate fields.
  • Comprehensive Error Handling: Provides detailed error messages for validation failures.
  • Type Constraints: Supports various types including enums, lists, tuples, and more.

Usage

To define a schema, use the defschema macro. By default, all fields in the schema are optional unless specified otherwise.

defmodule MySchemas do
  import Peri

  defschema :user, %{
    name: :string,
    age: :integer,
    email: {:required, :string},
    address: %{
      street: :string,
      city: :string
    },
    tags: {:list, :string},
    role: {:enum, [:admin, :user, :guest]},
    geolocation: {:tuple, [:float, :float]},
    rating: {:custom, &validate_rating/1}
  }

  defp validate_rating(n) when n < 10, do: :ok
  defp validate_rating(_), do: {:error, "invalid rating", []}
end

You can then use the schema to validate data:

user_data = %{
  name: "John", age: 30, email: "john@example.com",
  address: %{street: "123 Main St", city: "Somewhere"},
  tags: ["science", "funky"], role: :admin,
  geolocation: {12.2, 34.2}, rating: 9
}

case MySchemas.user(user_data) do
  {:ok, valid_data} -> IO.puts("Data is valid!")
  {:error, errors} -> IO.inspect(errors, label: "Validation errors")
end

Available Types

  • :string - Validates that the field is a binary (string).
  • :integer - Validates that the field is an integer.
  • :float - Validates that the field is a float.
  • :boolean - Validates that the field is a boolean.
  • :atom - Validates that the field is an atom.
  • :any - Allows any datatype.
  • {:required, type} - Marks the field as required and validates it according to the specified type.
  • :map - Validates that the field is a map.
  • {:either, {type_1, type_2}} - Validates that the field is either of type_1 or type_2.
  • {:oneof, types} - Validates that the field is at least one of the provided types.
  • {:list, type} - Validates that the field is a list with elements of the specified type.
  • {:tuple, types} - Validates that the field is a tuple with the specified types in sequence.
  • {:custom, anonymous_fun_arity_1} - Validates that the field passes the callback function.
  • {:custom, {MyModule, :my_validation}} - Validates using a function from a specific module.
  • {:custom, {MyModule, :my_validation, [arg1, arg2]}} - Same as above but allows extra arguments.
  • {:cond, condition, true_type, else_type} - Validates the field based on a condition.
  • {:dependent, field, condition, type} - Validates the field based on another field’s value.
  • {type, {:default, default}} - Sets a default value for a field if it is nil.

Error Handling

Peri provides detailed error messages that include the path to the invalid data, the expected and actual values, and custom error messages for custom validations.

Functions

Example

defmodule MySchemas do
  import Peri

  defschema :user, %{
    name: :string,
    age: :integer,
    email: {:required, :string}
  }
end

user_data = %{name: "John", age: 30, email: "john@example.com"}
case MySchemas.user(user_data) do
  {:ok, valid_data} -> IO.puts("Data is valid!")
  {:error, errors} -> IO.inspect(errors, label: "Validation errors")
end

Summary

Functions

Checks if the given data conforms to the specified schema.

Defines a schema with a given name and schema definition.

Generates sample data based on the given schema definition using StreamData.

Checks if the given data is an enumerable, specifically a map or a list.

Checks if the given data is a numeric value, specifically a integer or a float.

Checks if the given type as an atom is a numeric (integer or float).

Validates a given data map against a schema.

Validates a schema definition to ensure it adheres to the expected structure and types.

Functions

Checks if the given data conforms to the specified schema.

Parameters

  • schema: The schema definition to validate against.
  • data: The data to be validated.

Returns

  • true if the data conforms to the schema.
  • false if the data does not conform to the schema.

Examples

iex> schema = %{name: :string, age: :integer}
iex> data = %{name: "Alice", age: 30}
iex> Peri.conforms?(schema, data)
true

iex> invalid_data = %{name: "Alice", age: "thirty"}
iex> Peri.conforms?(schema, invalid_data)
false
Link to this macro

defschema(name, schema)

View Source (macro)

Defines a schema with a given name and schema definition.

Examples

defmodule MySchemas do
  import Peri

  defschema :user, %{
    name: :string,
    age: :integer,
    email: {:required, :string}
  }
end

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

invalid_data = %{name: "John", age: 30}
MySchemas.user(invalid_data)
# => {:error, [email: "is required"]}

Generates sample data based on the given schema definition using StreamData.

This function validates the schema first, and if the schema is valid, it uses the Peri.Generatable.gen/1 function to generate data according to the schema.

Note that this function returns a Stream, so you traverse easily the data generations.

Parameters

  • schema: The schema definition to generate data for.

Returns

  • {:ok, stream} if the data is successfully generated.
  • {:error, errors} if there are validation errors in the schema.

Examples

iex> schema = %{name: :string, age: {:integer, {:range, {18, 65}}}}
iex> {:ok, stream} = Peri.generate(schema)
iex> [data] = Enum.take(stream, 1)
iex> is_map(data)
true
iex> data[:age] in 18..65
true
Link to this macro

is_enumerable(data)

View Source (macro)

Checks if the given data is an enumerable, specifically a map or a list.

Parameters

  • data: The data to check.

Examples

iex> is_enumerable(%{})
true

iex> is_enumerable([])
true

iex> is_enumerable(123)
false

iex> is_enumerable("string")
false

Checks if the given data is a numeric value, specifically a integer or a float.

Parameters

  • data: The data to check.

Examples

iex> is_numeric(123)
true

iex> is_numeric(0xFF)
true

iex> is_numeric(12.12)
true

iex> is_numeric("string")
false

iex> is_numeric(%{})
false
Link to this macro

is_numeric_type(t)

View Source (macro)

Checks if the given type as an atom is a numeric (integer or float).

Parameters

  • data: The data to check.

Examples

iex> is_numeric(:integer)
true

iex> is_numeric(:float)
true

iex> is_numeric(:list)
false

iex> is_numeric({:enum, _})
false

Validates a given data map against a schema.

Returns {:ok, data} if the data is valid according to the schema, or {:error, errors} if there are validation errors.

Parameters

  • schema: The schema definition map.
  • data: The data map to be validated.

Examples

schema = %{
  name: :string,
  age: :integer,
  email: {:required, :string}
}

data = %{name: "John", age: 30, email: "john@example.com"}
Peri.validate(schema, data)
# => {:ok, %{name: "John", age: 30, email: "john@example.com"}}

invalid_data = %{name: "John", age: 30}
Peri.validate(schema, invalid_data)
# => {:error, [email: "is required"]}

Validates a schema definition to ensure it adheres to the expected structure and types.

This function can handle both simple and complex schema definitions, including nested schemas, custom validation functions, and various type constraints.

Parameters

  • schema - The schema definition to be validated. It can be a map or a keyword list representing the schema.

Returns

  • {:ok, schema} - If the schema is valid, returns the original schema.
  • {:error, errors} - If the schema is invalid, returns an error tuple with detailed error information.

Examples

Validating a simple schema:

  schema = %{
    name: :string,
    age: :integer,
    email: {:required, :string}
  }
  assert {:ok, ^schema} = validate_schema(schema)

Validating a nested schema:

  schema = %{
    user: %{
      name: :string,
      profile: %{
        age: {:required, :integer},
        email: {:required, :string}
      }
    }
  }
  assert {:ok, ^schema} = validate_schema(schema)

Handling invalid schema definition:

  schema = %{
    name: :str,
    age: :integer,
    email: {:required, :string}
  }
  assert {:error, _errors} = validate_schema(schema)