README.md

# Flawless

Flawless is an Elixir library to help validate user input against a schema.


```elixir
iex> import Flawless.Helpers

iex> schema = %{
...>   "username" => string(max_length: 30),
...>   "address" => %{
...>     "street" => string(),
...>     "number" => integer(min: 0, cast_from: :string),
...>     "city" => string(),
...>   },
...>   maybe("interests") => list(string(), min_length: 1)
...> }

iex> value = %{
...>   "address" => %{
...>     "country" => "Belgium",
...>     "number" => "10",
...>     "street" => "Main street",
...>     "city" => "Brussels"
...>   },
...>   "interests" => ["programming", "music", :games]
...> }

iex> Flawless.validate(value, schema)
[
  %Flawless.Error{
    context: [],
    message: "Missing required fields: \"username\" (string)."
  },
  %Flawless.Error{
    context: ["address"],
    message: "Unexpected fields: [\"country\"]."
  },
  %Flawless.Error{
    context: ["interests", 2],
    message: "Expected type: string, got: :games."
  }
]
```

**Note:** this is a personal project, which I released pretty recently. While it
is completed tested, it was never battle-tested. Use it carefully, and don't
hesitate to share bugs, ideas and your impressions.

## Why another validation library?

There are already a lot of validation libraries in Elixir. So why write another
one?

When I looked for other libraries, I found that there were a few recurrent
issues. Namely, poor error messages (not very suitable for user-facing
applications), and a cumbersome or inconsistent syntax. While they all have
their qualities, I wanted to try out something that was more to my taste.

Also, that was an occasion to learn more about Elixir and experiment.

As much as possible, this library tries to be:

- **Consistent:** all the helpers provide the same set of common options.
- **General:** some validation rules might be harder to define than others, but
  it avoids imposing any restriction.
- **Readable:** a lot of helpers and shortcuts are provided to make the schemas
  as simple as possible. The syntax is similar to the syntax of typespecs (when
  it makes sense) and should feel natural if you're used to it.
- **User-friendly:** useful errors are returned with clear messages and context,
  so that it should be easy for a user to understand how to fix the input.
- **Modular:** schemas are normal Elixir objects and can be easily combined
  together. No restricting syntax or macro is imposed.

## Schema definition

This section is only an overview. For details, see the dedicated
[Schema definition](guides/schema_definition.md) page.

### Helpers

Schemas are built using helper functions, that internally create more complex
structures used during validation. Every data type (string, number, map, list,
etc.) has its own helper function.

All helpers support a few common options:

- `checks` / `check` - See [Checks](#checks)
- `late_checks` / `late_check` - See [Checks](#checks)
- `nil` - See [Nullable values](#nullable-values)
- `cast_from` - See [Casting](#casting)
- `on_error` - See [Overriding error messages](#overriding-error-messages)

The `any()` helper is the only helper without a specific type. It can be used
to match literally anything.

### Checks

Every element can define a series of checks. Each check will evaluate a predicate
on the value and return an error message if it didn't pass. A few built-in rules
are available in `Flawless.Rule` though shortcuts are also available for them.

It is also possible to define your own rules easily using the `rule/2` helper or
a simple function that returns an `:ok`/`:error` tuple. For more information, see
the [Custom checks](guides/custom_checks.md) page.

Late checks allow to evaluate rules *after all other checks have passed*. This
is useful if the rule should only be evaluated on well-formed data.

```elixir
# An integer between 0 and 10
integer(checks: [between(0, 10)])

# Accepts only "yes", true, or 1
any(check: one_of(["yes", true, 1]))

# A number that is different from 0
number(check: rule(&(&1 != 0), "The number should be different from zero."))

# The late check never fails because we're sure keys a and b exist
map(
  %{a: number(), b: number()},
  late_check: rule(fn x -> x.a > x.b end, "a must be bigger than b.")
)
```

### Primitive types

Flawless supports all primitive Elixir types with the functions `integer/1`,
`float/1`, `number/1`, `string/1`, `boolean/1`, `atom/1`, `pid/1`, `ref/1`,
`function/1` and `port/1`. Each of them supports specific options, which are
shortcuts to avoid lengthy `checks`.

```elixir
# A non-empty string
string(non_empty: true)

# An integer between 0 and 100
integer(min: 0, max: 100)
```

### Nullable values

Every element supports the `nil` boolean option. When it is `true`, the element
can be `nil` even if that doesn't match any of the other constraints.

It is `false` by default, except for optional keys in maps.

### Maps

Maps are defined using `map/2` or directly with a map if no options are
necessary. By default, all keys are *required*, but optional keys can be defined
with the `maybe/1` helper. Non-specified keys are by default forbidden, but it
can be changed by adding the `any_key()` key in the map.

```elixir
map(
  %{
    # Define two required keys
    "id" => integer(),
    "name" => string(),

    # Define one optional field
    maybe("age") => integer(),

    # Accept any other key as long as the values are strings
    any_key() => string()
  },
  nil: false
)

# If we drop the `nil` option, we can ignore the `map()` function
%{
  "id" => integer(),
  "name" => string(),
  maybe("age") => integer(),
  any_key() => string()
}
```

### Structs

Structs work similarly to map but with the `structure/2` helper:

```elixir
structure(
  %Profile{id: integer(), username: string(), created_at: datetime()}
)

# Or just
%Profile{id: integer(), username: string(), created_at: datetime()}
```

Opaque structs can be checked by specifying only the module:

```elixir
structure(Profile)
```

### Lists

Lists are validated by providing a schema that every item must conform to:

```elixir
# A list of strings with at least two elements
list(string(), min_length: 2)

# A list of numbers (shortcut)
[number()]
```

### Tuples

Tuples are validated by providing a schema for each element:

```elixir
# A two-element tuple with an atom and a string
tuple({atom(), string()})

# A three-element tuple with three floats (shortcut)
{float(), float(), float()}
```

### Literals

Literal values (constants) are validated using the `literal/2` helper
or the value itself for numbers, atoms and strings:

```elixir
# Match the list `[1, 2, 3]`
literal([1, 2, 3])

# Match an {:ok, string} tuple (two alternatives)
{literal(:ok), string()}
{:ok, string()}
```

### Date & Time

Elixir has 4 built-in structs for date and time. They can be checked with the
`date()`, `time()`, `datetime()` and `naive_datetime()` helpers:

```elixir
# A DateTime before 1 January 2012 at 12:00:00
datetime(before: ~U[2012-01-01 12:00:00Z])

# A Time after 08:00
time(after: ~T[08:00:00])
```

### Recursive schemas

Recursive schemas can be defined by providing 0-arity functions:

```elixir
def tree_schema do
  %{
    value: number(),
    children: list(&tree_schema/0)
  }
end
```

### Unions

Unions can be defined in two ways:

1. Using the `union/1` helper:

  ```elixir
  union([string(non_empty: true), number(min: 0)])
  ```

2. Using 1-arity functions that decide which schema to use based on the input
   data:

  ```elixir
  %{
    # Metadata is either a map with string values, or a list of strings
    "metadata" => fn
      %{} -> map(%{any_key() => string()})
      [_ | _] -> list(string())
    end
  }
  ```

### Casting

Data can be automatically casted to the expected type if possible. The data is
validated after the casting has been performed:

```elixir
# Accept positive numbers, or strings representing positive numbers
number(cast_from: :string, min: 0)

# With a custom converter
map(%{"value" => number()}, cast_from: {:string, with: &Jason.decode/2})
```

### Overriding error messages

You replace all the errors on an element by a single error message with
`on_error`:

```elixir
iex> value = "xX-DarkL0rd-Xx"
iex> schema1 = string(format: ~r/^[a-zA-Z_]+$/)
iex> validate(value, schema1)
[
  %Flawless.Error{context: [], message: "Value \"xX-DarkL0rd-Xx\" does not match regex ~r/^[a-zA-Z_]+$/."}
]

iex> schema2 = string(format: ~r/^[a-zA-Z_]+$/, on_error: "The username should only contain letters or underscores.")
iex> validate(value, schema2)
[
  %Flawless.Error{context: [], message: "The username should only contain letters or underscores."}
]
```

## Validate data

You can validate data against a schema with the `validate/3` function. It 
returns a list of errors, which is empty if the data is valid.

```elixir
iex> schema = %{name: string(), age: number()}

iex> validate(%{name: :Colin}, schema)
[
  %Flawless.Error{context: [], message: "Missing required fields: :age (number)."},
  %Flawless.Error{context: [:name], message: "Expected type: string, got: :Colin."}
]

iex> validate(%{name: "Colin", age: 26}, schema)
[]
```

## Validate schemas

You can validate that a schema you're using is a valid schema with the
`validate_schema/1` function:

```elixir
schema = %{
  name: string(),
  age: number()
}

validate_schema(schema)
```

It returns the same kind of errors as `validate/3`. The "schema of a schema" is
actually defined using this library, and that schema validates itself.

By default, validating a value against a schema will validate the schema first.
If you wish to disable that behaviour (in particular if you can the function
many times with the same schema), set the `check_schema` option to false.

## Not what you need?

If you find a bug, or if you would like to propose improvements, please open an
issue or submit a PR.

If this library does not fit exactly your needs, check out those other validation
libraries. One of them might be best suited to your use case or preferences:

- [Vex](https://github.com/CargoSense/vex)
- [Norm](https://github.com/elixir-toniq/norm)
- [Skooma](https://github.com/bobfp/skooma)
- [Xema](https://github.com/hrzndhrn/xema)
- [ex_json_schema](https://github.com/jonasschmidt/ex_json_schema)
- [Exop](https://github.com/madeinussr/exop)
- [Joi](https://github.com/scottming/joi)
- [Optimal](https://hexdocs.pm/optimal/readme.html)
- [TypeCheck](https://github.com/Qqwy/elixir-type_check)

## License

The source code of Flawless is licensed under the MIT License.