/zbang

A schema description and data validation library for Elixir

Primary LanguageElixirMIT LicenseMIT

Z!

Z! is a schema description and data validation library. Inspired by libraries like Joi, Yup, and Zod from the JavaScript community, Z! helps you describe schemas for your structs and validate their data at runtime.

Installation

This package can be installed by adding zbang to your list of dependencies in mix.exs:

def deps do
  [
    {:zbang, "~> 1.1.1"}
  ]
end

The docs can be found at https://hexdocs.pm/zbang

Types

Many types can be validated with Z!. Below is a list of built-in primitive types, but you can also define custom types of your own.

Any

Module: Z.Any Shorthand: :any

Rules

  • :default - If the input is nil, sets input to given value
    • You may set the default to any literal value. When defined on a struct field, the value will be validated against the field type and all other field rules at compile time.
    • You may also set the default to any function with an arity of zero. The function will be evaluated at validation time and the returned value will be used as the default value. When defined on a struct field, the function must be a named function and will not be evaluated or validated at compile time.
  • :required - Asserts that input is not nil
  • :equals - Asserts that input is equal to given value
  • :enum - Asserts that input is in list of given values

Note: The rules for Z.Any may be used for all other types as well since every type is implicitly a Z.Any

Atom

Module: Z.Atom Shorthand: :atom

Rules

  • :parse - If input is a string, try to parse it to an atom

Boolean

Module: Z.Boolean Shorthand: :boolean

Rules

  • :parse - if input is a string, try to parse it to a boolean

Date

Module: Z.Date Shorthand: :date

Rules

  • :parse - If input is a string, try to parse it to a Date
  • :trunc - If input is a DateTime or NaiveDateTime, convert it to a Date
  • :min - Asserts that the input is at least the given Date or after
  • :max - Asserts that the input is at most the given Date or before

DateTime

Module: Z.DateTime Shorthand: :date_time

Rules

  • :parse - If input is a string, try to parse it to a DateTime
  • :allow_int - If input is an integer, try to convert it to a DateTime
  • :shift - Shift the input to the same point in time at the given timezone
  • :trunc - Truncates the microsecond field of the input to the given precision
  • :min - Asserts that the input is at least the given DateTime or after
  • :max - Asserts that the input is at most the given DateTime or before

Float

Module: Z.Float Shorthand: :float

Rules

  • :parse - If input is a string, try to parse it to a float
  • :allow_int - If input is an integer, convert it to a float
  • :min - Asserts that input is greater than or equal to given value
  • :max - Asserts that input is less than or equal to given value
  • :greater_than - Asserts that input is greater than given value
  • :less_than - Asserts that input is less than given value

Integer

Module: Z.Integer Shorthand: :integer

Rules

  • :parse - If input is a string, try to parse it to an integer
  • :trunc - If input is a float, truncate it to an integer
  • :min - Asserts that input is greater than or equal to given value
  • :max - Asserts that input is less than or equal to given value
  • :greater_than - Asserts that input is greater than given value
  • :less_than - Asserts that input is less than given value

List

Module: Z.List Shorthand: :list

Rules

  • :items - Validates the items in the input list
  • :length - Asserts that input length is equal to the given value
  • :min - Asserts that input length is at least the given length
  • :max - Asserts that input length is at most the given length

Map

Module: Z.Map Shorthand: :map

Rules

  • :atomize_keys - If key is a string, try to parse it to an atom (only existing atoms by default)
  • :size - Asserts that the input size is equal to the given value
  • :min - Asserts that the input size is at least the given value
  • :max - Asserts that the input size is at most the given value

String

Module: Z.String Shorthand: :string

Rules

  • :trim - Trims any leading or trailing whitespace from the input
  • :length - Asserts that input length is equal to the given value
  • :min - Asserts that input length is at least the given length
  • :max - Asserts that input length is at most the given length

Struct

Module: Z.Struct

Rules

  • :cast - If the input is a Map, try to cast it to the given struct

Note: Don't use Z.Struct directly. Instead, define your own struct with use Z.Struct and a schema block

Time

Module: Z.Time Shorthand: :time

Rules

  • :parse - If input is a string, try to parse it to a Time
  • :trunc - Truncates the microsecond field of the input to the given precision
  • :min - Asserts that the input is at least the given Time or after
  • :max - Asserts that the input is at most the given Time or before

Describing Schemas

Example

defmodule Money do
  use Z.Struct

  schema do
    field :amount, :float, [:required, :parse, min: 0.0]
    field :currency, :string, [:required, default: "USD", enum: ["USD", "EUR", "BTC"]]
  end
end

defmodule Book do
  use Z.Struct

  schema do
    field :title, :string, [:required]
    field :author, :string, [:required, default: "Unknown"]
    field :description, :string
    field :price, Money, [:required, :cast]
    field :read_at, :datetime, [default: &DateTime.utc_now/0]
  end
end

In the above example, we are defining two structs by employing use Z.Struct with a schema block where fields are defined. When you define a struct in this way, Z.Struct will call defstruct for you and create an Elixir struct with defaults when given. In addition, it will define a validate/3 function on your struct module that can be used to validate values at runtime.

The validate/3 function uses the fields defined in the schema block to automatically assert the type of each value as well as assert that the given rules are being followed.

In addition to the validate/3 function, new/1 and new!/1 functions are also added for instantiating your structs from a keyword list or any other key-value enumerable. These functions will also validate your newly created struct.

Each field takes a name, type and optional rules. The name must be an atom. The type must also be an atom and can either be a built-in type or a custom type e.g. the Money type used by the :price field in the example above. The rules vary depending on the type given. See here for a list of all rules per type.

All :required fields will be added to @enforce_keys by default. If you don't want to enforce a required field at compile time, you may opt out of this behavior with required: [enforce: false]

Validation

Validating data is as simple as calling validate/3 on the type that you would like to assert and passing in optional rules. The validate/3 function will return either {:ok, value} or {:error, error}.

Alternatively, you can use validate!/3 which has the same signature as validate/3 but returns the validated value instead of the {:ok, value} tuple. If there are issues during validation, validate!/3 will raise a Z.Error

Examples

Z.String.validate("hello world")
{:ok, "hello world"}

Z.String.validate("oops", length: 5)
{:error,
 %Z.Error{
   issues: [
     %Z.Issue{
       code: "too_small",
       message: "input does not have correct length",
       path: ["."]
     }
   ],
   message: "invalid"
 }}
 
Z.String.validate(nil, [:required, default: "sleepy bear"])
{:ok, "sleepy bear"}

Book.validate(%{title: "I <3 Elixir", price: %{amount: "1.00"}})
{:error,
 %Z.Error{
   issues: [
     %Z.Issue{code: "invalid_type", message: "input is not a Book", path: ["."]}
   ],
   message: "invalid"
 }}
 
Book.validate(%{title: "I <3 Elixir", price: %{amount: "1.00"}}, [:cast])
{:ok,
 %Book{
   author: "Unknown",
   description: nil,
   price: %Money{amount: 1.0, currency: "USD"},
   read_at: ~U[2022-07-19 04:14:58.979221Z],
   title: "I <3 Elixir"
 }}