README.md

# ExJoi

**Beautiful, declarative validation for Elixir**

ExJoi brings a Joi-inspired DSL to Elixir, letting you describe data rules once and trust the engine to enforce them everywhere—APIs, configs, forms, and beyond.

---

## Quick Links

- GitHub · https://github.com/abrshewube/ExJoi
- HexDocs (v0.3.0) · https://hexdocs.pm/exjoi/0.3.0
- Hex Package · https://hex.pm/packages/exjoi

---

## Highlights

- **Schema-first DSL** – Compose readable validation rules with `ExJoi.string/1`, `number/1`, `boolean/1`, and `object/1`.
- **Advanced constraints** – Min/max lengths, regex patterns, email format checks, integer guards, and truthy/falsy coercion.
- **Nested objects** – Recursively validate deep maps with rich, nested error payloads.
- **Smart defaults** – Provide top-level defaults that merge into incoming params before validation.
- **Actionable errors** – Structured responses include machine-friendly codes, friendly messages, and metadata.
- **Key-flexible** – Accepts atom or string keys seamlessly.

---

## Install

Add the dependency and you’re ready to validate:

```elixir
defp deps do
  [
    {:exjoi, "~> 0.3.0"}
  ]
end
```

---

## 60‑Second Tour

```elixir
schema =
  ExJoi.schema(
    %{
      user:
        ExJoi.object(%{
          name: ExJoi.string(required: true, min: 2, max: 50),
          email: ExJoi.string(required: true, email: true)
        }),
      stats: ExJoi.number(integer: true, min: 0),
      active: ExJoi.boolean(truthy: ["Y", "yes"], falsy: ["N", "no"])
    },
    defaults: %{active: true, stats: 0}
  )

case ExJoi.validate(%{"user" => %{"name" => "Maya", "email" => "maya@example.com"}}, schema) do
  {:ok, normalized} ->
    IO.inspect(normalized)

  {:error, %{message: msg, errors: errors}} ->
    IO.inspect({msg, errors})
end

# {:ok, %{"active" => true, "stats" => 0, "user" => %{"email" => "maya@example.com", "name" => "Maya"}}}
```

---

## Constraint Cheat Sheet

| Helper        | Options                                                                                     |
| ------------- | ------------------------------------------------------------------------------------------- |
| `ExJoi.string`  | `:required`, `:min`, `:max`, `:pattern` (`Regex`), `:email`                                 |
| `ExJoi.number`  | `:required`, `:min`, `:max`, `:integer`                                                     |
| `ExJoi.boolean` | `:required`, `:truthy`, `:falsy` (lists coerced to `true` / `false`)                        |
| `ExJoi.object`  | `:required` (accepts nested map or `%ExJoi.Schema{}`)                                       |

```elixir
ExJoi.string(required: true, min: 3, max: 32, pattern: ~r/^[a-z0-9_]+$/)
ExJoi.number(integer: true, min: 1)
ExJoi.boolean(truthy: ["1", "on"], falsy: ["0", "off"])
```

---

## Rich Error Format

Validation failures always follow the same envelope:

```elixir
{:error,
 %{
   message: "Validation failed",
   errors: %{
     name: [
       %{code: :required, message: "is required"}
     ],
     age: [
       %{code: :number_min, message: "must be greater than or equal to 18", meta: %{min: 18}}
     ]
   }
 }}
```

Each error entry includes:

- `code` – Atom identifier (`:required`, `:string_pattern`, `:boolean`, ...).
- `message` – Friendly sentence ready for users.
- `meta` – Optional context (`%{min: 18}`, `%{pattern: ...}`) for UI or logging.

---

## Recipes

### Validate credentials

```elixir
ExJoi.schema(%{
  username: ExJoi.string(required: true, min: 4, max: 32, pattern: ~r/^[a-z0-9_]+$/i),
  password: ExJoi.string(required: true, min: 8)
})
```

### Enforce price & quantity

```elixir
ExJoi.schema(%{
  price: ExJoi.number(required: true, min: 0),
  quantity: ExJoi.number(required: true, min: 1, max: 100, integer: true)
})
```

### Custom truthy/falsy

```elixir
ExJoi.schema(%{
  subscribed: ExJoi.boolean(truthy: ["Y", "yes"], falsy: ["N", "no"])
})
```

### Nested user profile

```elixir
ExJoi.schema(%{
  user:
    ExJoi.object(%{
      email: ExJoi.string(required: true, email: true),
      profile: ExJoi.object(%{bio: ExJoi.string(max: 140)})
    })
})
```

---

## Roadmap Snapshot

| Version | Status  | Highlights |
| ------- | ------- | ---------- |
| 3       | Current | Object schemas, nested validation, defaulting |
| 2       | Shipped | Advanced constraints, truthy/falsy coercion, structured errors |
| 4       | Planned | Array validation |
| 5       | Planned | Type coercion / casting |
| 6       | Planned | Conditional rules |
| 7       | Planned | Custom validators & plugin system |
| 8       | Planned | Full error tree & custom error builder |
| 9       | Planned | Async / parallel validation |
| 10      | Planned | Macro DSL, compiler, performance optimizations |

Version 1 delivered the foundational engine with basic types and required flags.

---

## Contributing

1. Fork and create a topical branch (e.g. `version-3-nested-schemas`).
2. Run `mix test` before opening a PR.
3. Document new DSL additions in the README / HexDocs.

---

## License

MIT © 2025 abrshewube — build wonderful validations!