From c85b9720e972ea50e6caec0d8a30d67301b7ce97 Mon Sep 17 00:00:00 2001 From: Zoey de Souza Pessanha Date: Thu, 27 Jun 2024 19:45:43 -0300 Subject: [PATCH] feat: data generation --- CHANGELOG.md | 6 + README.md | 386 ++++++++++++++++++++-------------------- lib/peri/generatable.ex | 78 +++++++- mix.exs | 2 +- 4 files changed, 274 insertions(+), 198 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bc7722..7711a90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## [0.2.6] - 2024-06-27 + +### Added + +- Data generation with based on `StreamData` provided as the `Peri.generate/1` function that receives a schema and returns a stream of generated data that matches this schema. + ## [0.2.5] - 2024-06-22 ### Added diff --git a/README.md b/README.md index f82ed4a..1cd0e8d 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,65 @@ -# Peri - Schema Validation Library for Elixir +# Peri -## General Description - -Peri is a schema validation library for Elixir, inspired by Clojure's Plumatic Schema. It allows developers to define schemas for validating various data structures, supporting nested schemas, optional fields, and custom validation types. Peri aims to provide an intuitive and flexible way to ensure data integrity in Elixir applications. +Peri is a schema validation library for Elixir, inspired by Clojure's Plumatic Schema. It provides a powerful and flexible way to define and validate schemas for your data, ensuring data integrity and consistency throughout your application. Peri supports a variety of types and validation rules, and it can generate sample data based on your schemas. ## Features -- Simple and intuitive syntax for defining schemas. -- Validation of data structures against schemas. -- Support for nested, composable, and recursive schemas. -- Optional and required fields. -- Comprehensive error handling with detailed messages. -- Flexible validation types, including custom and conditional validations. +- **Schema Definition**: Define schemas using a concise and expressive DSL. +- **Nested Schemas**: Support for deeply nested and complex schemas. +- **Custom Validation**: Implement custom validation functions for specific requirements. +- **Error Handling**: Detailed error messages with path information for easy debugging. +- **Data Generation**: Generate sample data based on your schemas using `StreamData`. ## Installation -To use Peri in your project, add it to your dependencies in `mix.exs`: - +Add this line to your `mix.exs`: ```elixir -def deps do +defp deps do [ - {:peri, "~> 0.2.3"} + {:peri, "~> 0.2"} ] end ``` -Then, run `mix deps.get` to fetch the dependencies. - -## Usage - -### Available Types - -Peri supports a variety of types to ensure your data is validated accurately. Below is a table summarizing the available types and their descriptions: - -| Type | Description | -|-------------------------------------------|-------------------------------------------------------------------------------------------------| -| `: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 without checking nested schema. | -| `{: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 where elements belong to a determined type. | -| `{:tuple, types}` | Validates that the field is a tuple with a determined size, and each element has its own type validation. | -| `{:custom, anonymous_fun_arity_1}` | Validates that the field passes the callback. The function needs to return either `:ok` or `{:error, template, info}` where `template` is an EEx string and `info` is a keyword list or map. | -| `{:custom, {MyModule, :my_validation}}` | Same as `{custom, anonymous_fun_arity_1}` but you pass a remote module and a function name as an atom. | -| `{:custom, {MyModule, :my_validation, [arg1, arg2]}}` | Same as `{:custom, {MyModule, :my_validation}}` but you can pass extra arguments to your validation function. Note that the value of the field is always the first argument. | -| `{:cond, condition, true_type, else_type}` | Conditionally validates a field based on the result of a condition function. | -| `{:dependent, field, condition, type}` | Validates a field based on the value of another field. | -| `{type, {:default, default}}` | Validates a field exists based on `type`, if not, return the `default` value | -| `{type, {:transform, mapper}}` | Validates a field have valid `type`, if yes, return the return of the `mapper/1` function passing the value | - -These types provide flexibility and control over how data is validated, enabling robust and precise schema definitions. - -### Defining and Validating Schemas - -Schemas can be defined using the `defschema` macro. By default, all fields in the schema are optional unless specified as `{:required, type}`. - -#### Example +## Available Types + +- `:any` - Allows any data type. +- `:atom` - Validates that the field is an atom. +- `:string` - Validates that the field is a binary (string). + - `{:regex, regex}` - Validates that the string field matches a given `regex` + - `{:eq, val}` - Validates that the string field is equal to `val` + - `{:min, min}` - Validates that the string field has at least the `min` length + - `{:max, max}` - Validates that the string field has at maximum the `max` length +- `:integer` - Validates that the field is an integer. + - `{:eq, val}` - Validates taht the integer field is equal to `val` + - `{:neq, val}` - Validates taht the integer field is not equal to `val` + - `{:lt, val}` - Validates taht the integer field is lesss than `val` + - `{:lte, val}` - Validates taht the integer field is less than or equal to `val` + - `{:gt, val}` - Validates taht the integer field is greater than `val` + - `{:gte, val}` - Validates taht the integer field is greater than or equal to `val` + - `{:range, {min, max}}` - Validates taht the integer field is inside the range of `min` to `max` (inclusive) +- `:float` - Validates that the field is a float. +- `:boolean` - Validates that the field is a boolean. +- `:map` - Validates that the field is a map. +- `{:required, type}` - Marks the field as required and validates it according to the specified type. +- `{:enum, choices}` - Validates that the field is one of the specified choices. +- `{:list, type}` - Validates that the field is a list of elements of the specified type. +- `{:tuple, types}` - Validates that the field is a tuple with elements of the specified types. +- `{type, {:default, default}}` - Provides a default value if the field is missing or `nil`. +- `{type, {:transform, mapper}}` - Transforms the field value using the specified mapper function. +- `{:either, {type1, type2}}` - Validates that the field is either of the two specified types. +- `{:oneof, types}` - Validates that the field is one of the specified types. +- `{:custom, callback}` - Validates that the field passes the custom validation function. +- `{:custom, {mod, fun}}` - Validates that the field passes the custom validation function. +- `{:custom, {mod, fun, args}}` - Validates that the field passes the custom validation function. +- `{:dependent, field, condition, type}` - Validates the field based on the value of another field. +- `{:cond, condition, type, else_type}` - Conditional validation based on a condition function. + +## Defining Schemas + +### Using the Macro + +You can define schemas using the `defschema` macro, which provides a concise syntax for defining and validating schemas. ```elixir defmodule MySchemas do @@ -69,7 +67,7 @@ defmodule MySchemas do defschema :user, %{ name: :string, - age: :integer, + age: {:integer, {:transform, & &1 * 2}}, email: {:required, :string}, address: %{ street: :string, @@ -84,230 +82,230 @@ defmodule MySchemas do defp validate_rating(n) when n < 10, do: :ok defp validate_rating(_), do: {:error, "invalid rating", []} end - -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 ``` -### `defschema` Macro General Explanation - -The `defschema` macro allows you to define a schema with a given name and schema definition. This macro injects functions that can validate data against the defined schema. +### Without the Macro -### Defining Schemas Without Macro - -You can also define schemas without using the `defschema` macro by directly passing the schema definition to the `Peri.validate/2` function. +You can also define schemas directly without using the macro: ```elixir -defmodule MySchemas do - @raw_user_schema %{age: :integer, name: :string} +schema = %{ + name: :string, + age: {:integer, {:transform, & &1 * 2}}, + email: {:required, :string} +} - def create_user(data) do - with {:ok, data} <- Peri.validate(@raw_user_schema, data) do - # rest of the function ... - end - end -end +Peri.validate(schema, %{name: "John", age: 30, email: "john@example.com"}) ``` -### Dynamic Schemas +## Composable and Reusable Schemas -Dynamic schemas can be generated based on runtime conditions. +Schemas can be composed and reused to build complex data structures. ```elixir defmodule MySchemas do import Peri - def generate_schema(is_admin) do - if is_admin do - %{ - role: {:required, :string}, - permissions: {:list, :string} - } - else - %{ - role: {:required, :string} - } - end - end - - def validate_user(data, is_admin) do - schema = generate_schema(is_admin) - Peri.validate(schema, data) - end -end + defschema :address, %{ + street: :string, + city: :string + } -data = %{role: "admin", permissions: ["read", "write"]} -case MySchemas.validate_user(data, true) do - {:ok, valid_data} -> IO.puts("Data is valid!") - {:error, errors} -> IO.inspect(errors, label: "Validation errors") + defschema :user, %{ + name: :string, + age: :integer, + email: {:required, :string}, + address: get_schema(:address) + } end ``` -### Nested and Composable Schemas +## Custom Validation Functions -Peri supports nested schemas, allowing for validation of complex data structures. +Implement custom validation functions to handle specific validation logic. + +The spec of the custom validation function is: +```elixir +@spec validation(term) :: :ok | {:error, template :: String.t(), context :: map | keyword} +``` + +Where `template` is a template string with the notation of `%{value}` where `value` is the name of the variable to be injected on the template. And `context` is a map or keyword list where the key is the name of the variable that will be injected into the template and the value is the value of this injected variable. Let's see an example: ```elixir defmodule MySchemas do import Peri - defschema :address, %{ - street: :string, - city: :string - } - defschema :user, %{ name: :string, - email: {:required, :string}, - address: {:custom, &address/1} + age: {:custom, &validate_age/1} } + + defp validate_age(age) when age >= 0 and age <= 120, do: :ok + defp validate_age(age), do: {:error, "invalid age, received: %{age}", [age: age]} end +``` -data = %{name: "John", email: "john@example.com", address: %{street: "123 Main St", city: "Somewhere"}} -case MySchemas.user(data) do +## Error Handling with Peri.Error + +Peri provides detailed error messages to help identify validation issues. Errors include path information to pinpoint the exact location of the error in the data structure. + +```elixir +case Peri.validate(schema, data) do {:ok, valid_data} -> IO.puts("Data is valid!") {:error, errors} -> IO.inspect(errors, label: "Validation errors") end ``` -### Recursive Schemas +## Data Generation -Recursive schemas allow you to define schemas that reference themselves, enabling the validation of nested and hierarchical data structures. - -```elixir -defmodule MySchemas do - import Peri +Peri can generate sample data based on your schemas using `StreamData`. - defschema :category, %{ - id: :integer, - name: :string, - subcategories: {:list, {:custom, &category/1}} - } +For this feature to work, ensures that you application depends on [stream_data](https://hexdocs.pm/stream_data). - category_data = %{ - id: 1, - name: "Electronics", - subcategories: [ - %{id: 2, name: "Computers", subcategories: []}, - %{id: 3, name: "Phones", subcategories: [%{id: 4, name: "Smartphones", subcategories: []}]} - ] - } +```elixir +schema = %{ + name: :string, + age: {:integer, {:gte, 18}}, + active: :boolean +} - case MySchemas.category(category_data) do - {:ok, valid_data} -> IO.puts("Category is valid!") - {:error, errors} -> IO.inspect(errors, label: "Validation errors") - end -end +sample_data = Peri.generate(schema) +Enum.take(sample_data, 10) # Generates 10 samples of the schema ``` -### Schemas on Raw Data Structures +## Perfect for Raw Data Structures + +Peri excels in validating raw data structures, such as tuples, strings, lists, and integers, with extensive validation options. This makes it ideal for use cases where you need to enforce strict data integrity rules on a wide variety of data types. Here's how Peri can help you handle these data structures: -Peri allows you to define schemas for various data structures, including lists, tuples, keyword lists, and primitive types. +### Tuples -#### Lists +Tuples can be validated for their structure and content, ensuring each element meets specific criteria. ```elixir defmodule MySchemas do import Peri - defschema :string_list, {:list, :string} - - data = ["hello", "world"] - case MySchemas.string_list(data) do - {:ok, valid_data} -> IO.puts("Data is valid!") - {:error, errors} -> IO.inspect(errors, label: "Validation errors") - end + defschema :coordinates, {:tuple, [:float, :float]} end + +data = {12.34, 56.78} +Peri.validate(get_schema(:coordinates), data) +# => {:ok, {12.34, 56.78}} + +invalid_data = {12.34, "not a float"} +Peri.validate(get_schema(:coordinates), invalid_data) +# => {:error, [%Peri.Error{message: "expected type of :float received \"not a float\" value"}]} ``` -#### Tuples +### Strings + +Strings can be validated for length, equality, and matching regular expressions. ```elixir defmodule MySchemas do import Peri - defschema :coordinates, {:tuple, [:float, :float]} - - data = {12.34, 56.78} - case MySchemas.coordinates(data) do - {:ok, valid_data} -> IO.puts("Data is valid!") - {:error, errors} -> IO.inspect(errors, label: "Validation errors") - end + defschema :username, {:string, {:regex, ~r/^[a-zA-Z0-9_]+$/}} end + +valid_data = %{username: "valid_user"} +Peri.validate(get_schema(:username), valid_data) +# => {:ok, %{username: "valid_user"}} + +invalid_data = %{username: "invalid user"} +Peri.validate(get_schema(:username), invalid_data) +# => {:error, [%Peri.Error{message: "should match the ~r/^[a-zA-Z0-9_]+$/ pattern"}]} ``` -#### Keyword Lists +### Lists + +Lists can be validated to ensure all elements are of a specific type and meet certain criteria. ```elixir defmodule MySchemas do import Peri - defschema :settings, [{:key, :string}, {:value, :any}] - - data = [key: "theme", value: "dark"] - case MySchemas.settings(data) do - {:ok, valid_data} -> IO.puts("Data is valid!") - {:error, errors} -> IO.inspect(errors, label: "Validation errors") - end + defschema :tags, {:list, :string} end + +valid_data = %{tags: ["elixir", "programming"]} +Peri.validate(get_schema(:tags), valid_data) +# => {:ok, %{tags: ["elixir", "programming"]}} + +invalid_data = %{tags: ["elixir", 42]} +Peri.validate(get_schema(:tags), invalid_data) +# => {:error, [%Peri.Error{message: "expected type of :string received 42 value"}]} ``` -### Error Handling +### Integers -Peri provides detailed error messages that can be easily inspected and transformed. Each error includes a message, content, path, key, and nested errors for detailed information about nested validation errors. +Integers can be validated for equality, inequality, and range constraints. ```elixir defmodule MySchemas do import Peri - defschema :user, %{ - name: :string, - age: {:required, :integer} - } - - data = %{name: "Jane"} - case MySchemas.user(data) do - {:ok, valid_data} -> IO.puts("Data is valid!") - {:error, errors} -> IO.inspect(errors, label: "Validation errors") - end + defschema :age, {:integer, {:range, {18, 65}}} end + +valid_data = %{age: 30} +Peri.validate(get_schema(:age), valid_data) +# => {:ok, %{age: 30}} + +invalid_data = %{age: 17} +Peri.validate(get_schema(:age), invalid_data) +# => {:error, [%Peri.Error{message: "should be in the range of 18..65 (inclusive)"}]} ``` -### InvalidSchema Exception +### Comprehensive Validation Options + +Peri's robust validation capabilities make it suitable for various data types and validation needs: -Peri raises an `InvalidSchema` exception when an invalid schema is encountered. This exception contains a list of `Peri.Error` structs, providing a readable message overview of the validation errors. +- **Equality and Inequality**: Validate that values match or do not match specific criteria. +- **Ranges**: Ensure numerical values fall within specified bounds. +- **Regular Expressions**: Enforce patterns on string data. +- **Custom Validation**: Implement complex rules through custom functions. -## Comparison with Ecto Schemaless Changesets and Embedded Schemas +By supporting these raw data structures and providing detailed error handling, Peri ensures that your data remains consistent and adheres to the defined rules, making it an excellent choice for applications requiring strict data validation. -### Peri +## Comparison with other data validation and mapping libraries -- **Purpose**: Designed specifically for schema validation. Focuses on validating raw maps against defined schemas. -- **Flexibility**: Allows easy validation of nested structures, optional fields, and dynamic schemas. -- **Simplicity**: The syntax for defining schemas is straightforward and intuitive. -- **Use Case**: Ideal for validating data structures in contexts where you don't need the full power of a database ORM. +### Peri vs. Norm -### Ecto Schemaless Changesets +**Norm** is another Elixir library for schema and data validation. While it shares some similarities with Peri, there are distinct differences: -- **Purpose**: Provides mechanisms for validating and casting data without persisting it to a database. -- **Complexity**: More complex due to its integration with Ecto and the need to handle changesets. -- **Schema Definitions**: Uses Ecto changesets and embedded schemas, which are typically tied to database schemas. -- **Use Case**: Ideal for applications that require validation and casting of data, even when it’s not being persisted to a database. +- **Focus**: Norm focuses on defining specifications and generating tests, offering a more comprehensive approach to data specifications, while Peri focuses on schema validation and transformations. +- **Validation**: Peri provides a more extensive set of built-in validations for strings, numbers, and custom types, whereas Norm allows for more generic and composable specifications. + +### Peri vs. Drops + +**Drops** is another Elixir library designed for validating and casting data. Key differences include: + +- **Schema Definition**: Drops uses a different syntax and approach for defining schemas. Peri provides a more familiar and flexible schema definition approach, not relying on macros. +- **Validation Capabilities**: Peri offers more advanced validation features, such as default values, transformations, and custom validations. +- **Error Handling**: Both libraries provide detailed error handling, but Peri's `Peri.Error` struct offers more context and customization options for error messages. + +### Peri vs. Ecto Schemaless Changesets + +**Ecto** is a powerful data mapping and query generator for Elixir, and it offers schemaless changesets for validating data without defining database schemas. + +- **Flexibility**: Peri offers more flexibility in defining schemas for various data structures, such as tuples, nested maps, and lists, along with detailed validation options. +- **Composable Validations**: Peri allows for more composable and reusable schemas, making it ideal for scenarios where data structures are complex and nested. + +### Peri vs. Ecto Embedded Changesets + +Ecto embedded changesets are used for validating and casting nested structures within Ecto schemas. + +- **Nested Structures**: Both Peri and Ecto handle nested structures well. However, Peri provides more granular control over validation rules for different types of nested data. +- **Usage Context**: Ecto embedded changesets are tightly integrated with Ecto schemas and structs, whereas Peri can be used independently of any kind of data structure or type, making it more versatile for various use cases. +- **Custom Validations**: Peri's custom validation functions and integration with `StreamData` for data generation provide additional capabilities not available in Ecto. ### Summary -- Use **Peri** if you need a lightweight, flexible library for validating raw maps and nested data structures without the overhead of database interactions. -- Use **Ecto Schemaless Changesets** if you need to validate and cast data in an Ecto-based application, leveraging the full power of Ecto’s changeset functionality. +While all these libraries offer data validation capabilities, Peri stands out with its flexibility, comprehensive validation options, and integration with StreamData for data generation. Whether you're dealing with raw data structures, need advanced validation features, or want to generate test data, Peri provides a robust and versatile solution tailored to meet these needs. + +## Why the Name "Peri"? + +The name "Peri" is derived from the Greek word "περί" (pronounced "peri"), which means "around" or "about." This name was chosen to reflect the library's primary purpose: to provide comprehensive and flexible schema validation for data structures in Elixir. Just as "peri" suggests encompassing or surrounding something, Peri aims to cover all aspects of data validation, ensuring that data conforms to specified rules and constraints. +The choice of the name "Peri" also hints at the library's ability to handle a wide variety of data types and structures, much like how the term "around" can denote versatility and inclusiveness. Whether it's validating nested maps, complex tuples, or strings with specific patterns, Peri is designed to be a robust tool that can adapt to various validation needs in Elixir programming. diff --git a/lib/peri/generatable.ex b/lib/peri/generatable.ex index 181dd90..ec83ac1 100644 --- a/lib/peri/generatable.ex +++ b/lib/peri/generatable.ex @@ -1,13 +1,85 @@ if Code.ensure_loaded?(StreamData) do defmodule Peri.Generatable do + @moduledoc """ + A module for generating sample data based on Peri schemas using StreamData. + + This module provides functions to generate various types of data, conforming to the schema definitions given in Peri. It leverages the StreamData library to create streams of random data that match the specified types and constraints. + + ## Examples + + iex> schema = %{ + ...> name: :string, + ...> age: {:integer, {:gte, 18}}, + ...> active: :boolean + ...> } + iex> Peri.Generatable.gen(schema) + %StreamData{ + type: :fixed_map, + data: %{name: StreamData.string(:alphanumeric), age: StreamData.filter(StreamData.integer(), &(&1 >= 18)), active: StreamData.boolean()} + } + + """ + require Peri + @doc """ + Generates a stream of data based on the given schema type. + + This function provides various clauses to handle different types and constraints defined in Peri schemas. It uses StreamData to generate streams of random data conforming to the specified types and constraints. + + ## Parameters + + - `schema`: The schema type to generate data for. It can be a simple type like `:integer`, `:string`, etc., or a complex type with constraints like `{:integer, {:gte, 18}}`. + + ## Returns + + - A StreamData generator stream for the specified schema type. + + ## Examples + + iex> Peri.Generatable.gen(:atom) + %StreamData{type: :atom, data: ...} + + iex> Peri.Generatable.gen(:string) + %StreamData{type: :string, data: ...} + + iex> Peri.Generatable.gen(:integer) + %StreamData{type: :integer, data: ...} + + iex> Peri.Generatable.gen({:enum, [:admin, :user, :guest]}) + %StreamData{type: :one_of, data: ...} + + iex> Peri.Generatable.gen({:list, :integer}) + %StreamData{type: :list_of, data: ...} + + iex> Peri.Generatable.gen({:tuple, [:string, :integer]}) + %StreamData{type: :tuple, data: ...} + + iex> Peri.Generatable.gen({:integer, {:gt, 10}}) + %StreamData{type: :filter, data: ...} + + iex> Peri.Generatable.gen({:string, {:regex, ~r/^[a-z]+$/}}) + %StreamData{type: :filter, data: ...} + + iex> Peri.Generatable.gen({:either, {:integer, :string}}) + %StreamData{type: :one_of, data: ...} + + iex> Peri.Generatable.gen({:custom, {MyModule, :my_fun}}) + %StreamData{type: :filter, data: ...} + + iex> schema = %{name: :string, age: {:integer, {:gte, 18}}} + iex> Peri.Generatable.gen(schema) + %StreamData{type: :fixed_map, data: ...} + + """ def gen(:atom), do: StreamData.atom(:alphanumeric) def gen(:string), do: StreamData.string(:alphanumeric) def gen(:integer), do: StreamData.integer() def gen(:float), do: StreamData.float() def gen(:boolean), do: StreamData.boolean() + def gen({:required, type}), do: gen(type) + def gen({:enum, choices}) do choices |> Enum.map(&StreamData.constant/1) @@ -101,7 +173,7 @@ if Code.ensure_loaded?(StreamData) do end def gen({:custom, {mod, fun}}) do - Stream.filter(StreamData.term(), fn val -> + StreamData.filter(StreamData.term(), fn val -> case apply(mod, fun, [val]) do :ok -> true {:ok, _} -> true @@ -111,7 +183,7 @@ if Code.ensure_loaded?(StreamData) do end def gen({:custom, {mod, fun, args}}) do - Stream.filter(StreamData.term(), fn val -> + StreamData.filter(StreamData.term(), fn val -> case apply(mod, fun, [val | args]) do :ok -> true {:ok, _} -> true @@ -121,7 +193,7 @@ if Code.ensure_loaded?(StreamData) do end def gen({:custom, cb}) do - Stream.filter(StreamData.term(), fn val -> + StreamData.filter(StreamData.term(), fn val -> case cb.(val) do :ok -> true {:ok, _} -> true diff --git a/mix.exs b/mix.exs index 99f9a77..1fd72ba 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Peri.MixProject do use Mix.Project - @version "0.2.5" + @version "0.2.6" @source_url "https://github.com/zoedsoupe/peri" def project do