Shapex

Shapex is a small library to define contracts for a maps and validate them easily. Key features:

Future plans

Example

defmodule UserValidator do
  alias Shapex.Types, as: S
  # or import Shapex.Types
  @user_schema S.map(%{
                 name: S.string(min_length: 3),
                 age: S.integer(gte: {18, "Should be adult"}),
                 email: S.string(regex: ~r/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/),
                 address:
                   S.map(%{
                     street: S.string(),
                     city: S.string(),
                     zip: S.string(min_length: 5)
                   }),
                 role: S.atom(eq: :admin)
               })

  def validate_user(user_params) do
    case Shapex.validate(@user_schema, user_params) do
      :ok -> insert_user(user)
      {:error, errors} ->
        Log.error("Validation failed", %{errors: errors})
        {:error, errors}
    end
  end
end

user = %{
  name: "John Doe",
  age: 17,
  email: "john@google.com",
  address: %{
    street: "123 Main St",
    city: "New York",
    zip: "000504"
  },
  role: :member
}

UserValidator.validate_user(user)

# {:error, %{age: %{gte: "Should be adult"}, role: %{eq: "Should be :admin"}}

Schema DSL

This DSL is used to simplify the creation of the contract that will be used for data validation. Here is an example of how it simplifies code.

Function composition:

alias Shapex.Types, as: S

animal_schema = S.enum([
  S.map(%{
    name: S.string(min_length: 2),
    family: S.atom(eq: :dog),
    breed: S.enum([S.string(eq: "Akita"), S.string(eq: "Husky"), S.string(eq: "Poodle")])
  }),
  S.map(%{
    name: S.string(min_length: 2),
    family: S.atom(eq: :cat),
    breed: S.enum([S.string(eq: "Siamese"), S.string(eq: "Persian"), S.string(eq: "Maine Coon")])
  }),
  S.map(%{
    name: S.string(min_length: 2),
    family: S.atom(eq: :owl),
    genus: S.enum([S.string(eq: "Athene"), S.string(eq: "Bubo"), S.string(eq: "Strix")])
  })
])

Schema DSL:

require Shapex

animal_schema = Shapex.schema(
  %{
    name: string(min_length: 2),
    family: :dog,
    breed: "Akita" | "Husky" | "Poodle"
  }
  | %{
    name: string(min_length: 2),
    family: :cat,
    breed: "Siamese" | "Persian" | "Maine Coon"
  }
  | %{
    name: string(min_length: 2),
    family: :owl,
    genus: "Athene" | "Bubo" | "Strix"
  }
)

As you can see you can use built-in function without any import, since they are supported by the DSL. They copy API of Shapex.Types module functions.

DSL Cheatsheet

The differences between schema DSL and default function composition style are:

Type DSL expression Function Composition
Integer 1integer(eq: 1)
Float 1.0float(eq: 1.0)
Boolean trueboolean(true)
Atom :atomatom(eq: :atom)
String "name"string(eq: "name")
Enum 1 | 2 | 3enum([integer(eq: 1), integer(eq: 2), integer(eq: 3)])
Map %{name: "John Doe"}map(key: string(eq: "John Doe"))

What is not planned