Current section

Files

Jump to
skema lib skema.ex
Raw

lib/skema.ex

defmodule Skema do
@moduledoc """
Skema is a simple schema validation and casting library for Elixir.
Provides four main APIs:
1. `cast_and_validate/2` - casting and validating data with given schema
2. `cast/2` - casting data with given schema
3. `validate/2` - validating data with given schema
4. `transform/2` - transforming data with given schema
## Define schema
Skema schema can be a map with field name as key and field definition as value,
or a schema module.
```elixir
schema = %{
email: [type: :string, required: true],
age: [type: :integer, number: [min: 18]],
hobbies: [type: {:array, :string}]
}
```
or using defschema:
```elixir
defmodule UserSchema do
use Skema
defschema do
field :email, :string, required: true
field :age, :integer, number: [min: 18]
field :hobbies, {:array, :string}
end
end
```
## Data Processing Pipeline
Skema provides a complete data processing pipeline:
1. **Cast** - Convert raw input to proper types
2. **Validate** - Check business rules and constraints
3. **Transform** - Normalize, format, and compute derived values
```elixir
# Full pipeline
raw_data = %{"email" => " JOHN@EXAMPLE.COM ", "age" => "25"}
with {:ok, cast_data} <- Skema.cast(raw_data, schema),
:ok <- Skema.validate(cast_data, schema),
{:ok, final_data} <- Skema.transform(cast_data, transform_schema) do
{:ok, final_data}
end
# Or use the combined function
case Skema.cast_and_validate(raw_data, schema) do
{:ok, data} -> IO.puts("Data is valid")
{:error, errors} -> IO.puts(inspect(errors))
end
```
## Transformation Features
Transform allows you to modify data after casting and validation:
```elixir
transform_schema = %{
email: [into: &String.downcase/1],
full_name: [
into: fn _value, data ->
"\#{data.first_name} \#{data.last_name}"
end
],
user_id: [as: :id, into: &generate_uuid/1]
}
```
### Transformation Options
- `into` - Function to transform the field value
- `as` - Rename the field in the output
### Function Types
```elixir
# Simple transformation
[into: &String.upcase/1]
# Access to all data
[into: fn value, data -> transform_with_context(value, data) end]
# Module function
[into: {MyModule, :transform_field}]
# Error handling
[into: fn value ->
if valid?(value) do
{:ok, normalize(value)}
else
{:error, "invalid value"}
end
end]
```
## API Differences
- **cast/2** - Type conversion only, returns `{:ok, data}` or `{:error, result}`
- **validate/2** - Rule checking only, returns `:ok` or `{:error, result}`
- **transform/2** - Data transformation, returns `{:ok, data}` or `{:error, result}`
- **cast_and_validate/2** - Combined cast + validate, returns `{:ok, data}` or `{:error, errors}`
"""
alias Skema.Caster
alias Skema.Result
alias Skema.Transformer
alias Skema.Validator
@doc false
defmacro __using__(_) do
quote do
import Skema.Schema, only: [defschema: 1, defschema: 2]
end
end
@doc """
Cast and validate data with given schema.
This function performs casting and validation in a single pass for optimal performance,
validating each field immediately after successful casting.
Returns `{:ok, data}` if both casting and validation succeed,
`{:error, errors}` otherwise.
"""
@spec cast_and_validate(data :: map(), schema :: map() | module()) ::
{:ok, map()} | {:error, errors :: map()}
def cast_and_validate(data, schema) do
cast(data, schema)
end
@doc """
Cast data to proper types according to schema.
Returns `{:ok, data}` if casting succeeds, `{:error, result}` otherwise.
"""
@spec cast(data :: map(), schema :: map() | module()) ::
{:ok, map()} | {:error, %Result{}}
def cast(data, schema) when is_atom(schema) do
schema.cast(data)
end
def cast(data, schema) when is_map(data) and is_map(schema) do
schema
|> build_initial_result(data)
|> Caster.process_cast_and_validate()
end
@doc """
Validate data according to schema rules.
Returns `:ok` if validation succeeds, `{:error, result}` otherwise.
"""
@spec validate(data :: map(), schema :: map() | module()) ::
:ok | {:error, %Result{}}
def validate(data, schema) when is_atom(schema) do
schema.validate(data)
end
def validate(data, schema) when is_map(data) and is_map(schema) do
schema
|> build_validation_result(data)
|> Validator.process_validation()
end
@doc """
Transform data according to schema transformation rules.
Supports multiple function signatures:
- `transform(%SomeSchema{} = data)` - Transform struct using its schema's __fields__()
- `transform(data, schema_module)` - Transform data using a schema module
- `transform(data, schema_map)` - Transform data using a schema map
- `transform(data, schema_keyword_list)` - Transform data using a keyword list schema
## Examples
# Using struct with schema
user = %UserSchema{name: "john", email: "JOHN@EXAMPLE.COM"}
{:ok, transformed} = Skema.transform(user)
# => %{name: "JOHN", email: "john@example.com"}
# Using schema module
data = %{name: "john", email: "JOHN@EXAMPLE.COM"}
{:ok, transformed} = Skema.transform(data, UserSchema)
# Using schema map
schema = %{name: [into: &String.upcase/1]}
{:ok, transformed} = Skema.transform(data, schema)
## Transformation Context
When transformation functions access the `data` parameter, they receive the
**original input data**, not data that has been transformed by other fields.
This ensures transformations are independent and deterministic.
schema = %{
name: [into: &String.upcase/1],
display: [into: &String.upcase/1]
}
data = %{name: "john"}
{:ok, result} = Skema.transform(data, schema)
# result.display will be "Name: john", not "Name: JOHN"
Returns `{:ok, data}` if transformation succeeds, `{:error, result}` otherwise.
"""
@spec transform(data :: map() | struct(), schema :: map() | module()) ::
{:ok, map()} | {:error, %Result{}}
def transform(data, schema) when is_atom(schema) do
schema.transform(data)
end
def transform(data, schema) when is_map(data) and is_map(schema) do
schema
|> build_transformation_result(data)
|> Transformer.process_transformation()
end
def expand(schema) do
Skema.SchemaHelper.expand(schema)
end
defp build_initial_result(schema, data) do
Result.new(schema: schema, params: data)
end
defp build_validation_result(schema, data) do
Result.new(schema: schema, params: data, valid_data: data)
end
defp build_transformation_result(schema, data) do
valid_data = if is_struct(data), do: Map.from_struct(data), else: data
Result.new(schema: schema, params: data, valid_data: valid_data)
end
end