README.md

# Z!

Z! is a schema description and data validation library.
Inspired by libraries like [Joi](https://joi.dev/), [Yup](https://github.com/jquense/yup), and [Zod](https://zod.dev/) 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`:

```elixir
def deps do
  [
    {:zbang, "~> 1.0.0"}
  ]
end
```

The docs can be found at [https://hexdocs.pm/zbang](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
- `: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_
```elixir
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]
  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` function on your struct module that can be used to validate values at runtime.

The `validate` 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.

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](#types) for a list of all rules per type.

## Validation

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

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

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

Book.validate(%{title: "I <3 Elixir", price: %{amount: "1.00"}})
{:error,
 [
   %Z.Error{
     code: "invalid_type",
     message: "input is not a Book",
     path: ["."]
   }
 ]}
 
Book.validate(%{title: "I <3 Elixir", price: %{amount: "1.00"}}, [:cast])
{:ok,
 %Book{
   author: "Unknown",
   description: nil,
   price: %Money{amount: 1.0, currency: "USD"},
   title: "I <3 Elixir"
 }}
```