Packages
raggio_schema
0.1.0
Composable schema definition and validation library for Elixir
Retired package: Release invalid - Published by mistake
Current section
Files
Jump to
Current section
Files
lib/raggio_schema.ex
defmodule Raggio.Schema do
@moduledoc """
Composable schema definition and validation library.
Raggio.Schema provides a function-based API for defining data schemas and validating data.
Type constructors accept keyword options for constraints:
Schema.string(min: 3, max: 20)
Schema.integer(min: 0, max: 100)
Schema.list(Schema.string(), min: 1, unique: true)
Schema.struct([
{:name, Schema.string(min: 1)},
{:age, Schema.integer(min: 0)},
{:email, Schema.optional(Schema.string(pattern: Schema.email()))}
])
Core Constraints: min, max, pattern, unique
Field Descriptors: optional/1, nullable/1, default: keyword option
"""
defstruct [
:type,
:encoded,
:inner_type,
:types,
:values,
constraints: [],
fields: nil,
optional: false,
nullable: false,
default: nil,
annotations: %{}
]
@type t :: %__MODULE__{
type: atom(),
encoded: atom() | nil,
constraints: keyword(),
fields: [{atom(), t()}] | nil,
inner_type: t() | nil,
types: [t()] | nil,
values: [any()] | nil,
optional: boolean(),
nullable: boolean(),
default: any(),
annotations: map()
}
@type validation_result :: {:ok, any()} | {:error, [validation_error()]}
@type validation_error :: %{
path: [atom() | integer()],
message: String.t(),
value: any(),
constraint: atom() | nil
}
def string(opts \\ []) do
{constraints, other_opts} = extract_constraints(opts, [:min, :max, :pattern])
%__MODULE__{
type: :string,
encoded: :string,
constraints: constraints,
default: Keyword.get(other_opts, :default)
}
end
def integer(opts \\ []) do
{constraints, other_opts} = extract_constraints(opts, [:min, :max])
%__MODULE__{
type: :integer,
encoded: :integer,
constraints: constraints,
default: Keyword.get(other_opts, :default)
}
end
def float(opts \\ []) do
{constraints, other_opts} = extract_constraints(opts, [:min, :max])
%__MODULE__{
type: :float,
encoded: :float,
constraints: constraints,
default: Keyword.get(other_opts, :default)
}
end
def boolean(opts \\ []) do
%__MODULE__{
type: :boolean,
encoded: :boolean,
default: Keyword.get(opts, :default)
}
end
def date(opts \\ []) do
%__MODULE__{
type: :date,
encoded: :date,
default: Keyword.get(opts, :default)
}
end
def datetime(opts \\ []) do
%__MODULE__{
type: :datetime,
encoded: :datetime,
default: Keyword.get(opts, :default)
}
end
def decimal(opts \\ []) do
{constraints, other_opts} = extract_constraints(opts, [:min, :max])
%__MODULE__{
type: :decimal,
encoded: :decimal,
constraints: constraints,
default: Keyword.get(other_opts, :default)
}
end
def atom(opts \\ []) do
%__MODULE__{
type: :atom,
encoded: :atom,
default: Keyword.get(opts, :default)
}
end
def struct(fields) when is_list(fields) do
%__MODULE__{
type: :struct,
encoded: :map,
fields: fields
}
end
def list(inner_schema, opts \\ [])
def list(inner_schema, opts) when is_struct(inner_schema, __MODULE__) do
{constraints, other_opts} = extract_constraints(opts, [:min, :max, :unique])
%__MODULE__{
type: :list,
encoded: :list,
inner_type: inner_schema,
constraints: constraints,
default: Keyword.get(other_opts, :default)
}
end
def tuple(schemas) when is_list(schemas) do
%__MODULE__{
type: :tuple,
encoded: :tuple,
types: schemas
}
end
def union(schemas) when is_list(schemas) and length(schemas) >= 2 do
%__MODULE__{
type: :union,
encoded: :any,
types: schemas
}
end
def literal(value) do
%__MODULE__{
type: :literal,
encoded: :any,
values: [value]
}
end
def literal(value1, value2) do
%__MODULE__{
type: :literal,
encoded: :any,
values: [value1, value2]
}
end
def literal(value1, value2, value3) do
%__MODULE__{
type: :literal,
encoded: :any,
values: [value1, value2, value3]
}
end
def literal(value1, value2, value3, value4) do
%__MODULE__{
type: :literal,
encoded: :any,
values: [value1, value2, value3, value4]
}
end
def literal(value1, value2, value3, value4, value5) do
%__MODULE__{
type: :literal,
encoded: :any,
values: [value1, value2, value3, value4, value5]
}
end
def record(key_schema, value_schema)
when is_struct(key_schema, __MODULE__) and is_struct(value_schema, __MODULE__) do
%__MODULE__{
type: :record,
encoded: :map,
fields: [key: key_schema, value: value_schema]
}
end
def optional(schema) when is_struct(schema, __MODULE__) do
%{schema | optional: true}
end
def nullable(schema) when is_struct(schema, __MODULE__) do
%{schema | nullable: true}
end
def email do
~r/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
end
def url do
~r/^https?:\/\/.+/
end
def uuid do
~r/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
end
def annotate(schema, annotations) when is_struct(schema, __MODULE__) and is_map(annotations) do
%{schema | annotations: Map.merge(schema.annotations, annotations)}
end
def validate(schema, data) when is_struct(schema, __MODULE__) do
Raggio.Schema.Validator.validate(schema, data)
end
def validate(schema, data, opts) when is_struct(schema, __MODULE__) and is_list(opts) do
Raggio.Schema.Validator.validate(schema, data, opts)
end
def validate!(schema, data) when is_struct(schema, __MODULE__) do
case validate(schema, data) do
{:ok, validated} -> validated
{:error, errors} -> raise Raggio.Schema.ValidationError, errors: errors
end
end
defp extract_constraints(opts, constraint_keys) do
constraints =
opts
|> Keyword.take(constraint_keys)
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
other_opts = Keyword.drop(opts, constraint_keys)
{constraints, other_opts}
end
end