README.md

# Mold

A tiny, zero-dependency parsing library for external payloads.

Mold parses JSON APIs, webhooks, HTTP params and other external input into clean Elixir terms.

## Philosophy

External data crosses the boundary as strings and maps with string keys. Before you can work with it, you need to turn `%{"age" => "25"}` into `%{age: 25}` — coerce types, rename keys, check structure. Mold does this in one step with `parse/2`.

Mold follows the [Parse, don't validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/) approach: instead of checking data and returning a boolean, `parse/2` transforms untyped input into well-typed output or returns structured errors. There is no `Mold.valid?/2`. You parse at the boundary, and from that point on you work with clean Elixir terms.

This doesn't mean the data is "valid" in every sense — a record might already exist in the database, a token might have expired, a concurrent process might have changed things. Mold handles structural correctness: types, shapes, constraints. Business logic is a separate layer.

Types in Mold are plain data. A type is just a value: you can build it at runtime, store in a variable, compose dynamically, or pass between modules.

## Installation

```elixir
def deps do
  [
    {:mold, "~> 0.1.0"}
  ]
end
```

## Quick start

```elixir
# Primitives
Mold.parse(:string, "  hello  ")        #=> {:ok, "hello"}
Mold.parse(:integer, "42")              #=> {:ok, 42}
Mold.parse(:boolean, "true")            #=> {:ok, true}
Mold.parse(:date, "2024-01-02")         #=> {:ok, ~D[2024-01-02]}

# Maps — string keys are the default
Mold.parse(%{name: :string, age: :integer}, %{"name" => "Alice", "age" => "25"})
#=> {:ok, %{name: "Alice", age: 25}}

# Lists
Mold.parse([:string], ["a", "b", "c"])  #=> {:ok, ["a", "b", "c"]}

# Custom parse functions
Mold.parse(&Version.parse/1, "1.0.0")   #=> {:ok, %Version{major: 1, minor: 0, patch: 0}}

# Options
Mold.parse({:integer, min: 0, max: 100}, "50")              #=> {:ok, 50}
Mold.parse({:string, nilable: true}, "")                     #=> {:ok, nil}
Mold.parse({:integer, default: 0}, nil)                      #=> {:ok, 0}
Mold.parse({:string, transform: &String.downcase/1}, "HI")   #=> {:ok, "hi"}
Mold.parse({:atom, in: [:draft, :published]}, "draft")       #=> {:ok, :draft}

# Errors include the path to the failing value
Mold.parse(%{items: [%{name: :string}]}, %{"items" => [%{"name" => "A"}, %{}]})
#=> {:error, [%Mold.Error{reason: {:missing_field, "name"}, trace: ["items", 1], ...}]}
```

## Types

A Mold type is plain Elixir data. Every type is one of three things:

| Form | Example | Meaning |
|---|---|---|
| Atom | `:string` | Built-in type with default options |
| Function | `&MyApp.parse_email/1` | Custom parse `fn value -> {:ok, v} \| {:error, r} \| :error end` |
| Tuple | `{:integer, min: 0}` | Type (atom or function) with options |

Maps and lists have a shortcut syntax:

| Shortcut | Example | Expands to |
|---|---|---|
| Map | `%{name: :string}` | `{:map, fields: [name: :string]}` |
| List | `[:string]` | `{:list, type: :string}` |

These forms compose into any shape:

```elixir
%{
  name: :string,
  age: {:integer, min: 0},
  address: %{city: :string, zip: :string},
  tags: [:string]
}
```

See [the documentation](https://hexdocs.pm/mold) for the full guide, types reference, and options.

## License

Apache-2.0