Sinter (Sinter v0.0.1)

View Source

Unified schema definition, validation, and JSON generation for Elixir.

Sinter provides a focused, high-performance schema validation library designed specifically for dynamic frameworks. It follows the "One True Way" principle:

  • One way to define schemas (unified core engine)
  • One way to validate data (single validation pipeline)
  • One way to generate JSON Schema (unified generator)

Quick Start

# Define a schema
fields = [
  {:name, :string, [required: true]},
  {:age, :integer, [optional: true, gt: 0]}
]
schema = Sinter.Schema.define(fields)

# Validate data
{:ok, validated} = Sinter.Validator.validate(schema, %{
  name: "Alice",
  age: 30
})

# Generate JSON Schema
json_schema = Sinter.JsonSchema.generate(schema)

Dynamic Schema Creation

Sinter supports dynamic schema creation for teleprompters and runtime optimization:

# Infer schema from examples (perfect for MIPRO teleprompter)
examples = [
  %{"name" => "Alice", "age" => 30},
  %{"name" => "Bob", "age" => 25}
]
schema = Sinter.infer_schema(examples)

# Merge schemas for signature composition
input_schema = Sinter.Schema.define([{:query, :string, [required: true]}])
output_schema = Sinter.Schema.define([{:answer, :string, [required: true]}])
program_schema = Sinter.merge_schemas([input_schema, output_schema])

Convenience Helpers

This module provides convenient helper functions for common validation tasks that internally use the core unified engine.

Design Philosophy

Sinter distills data validation to its pure essence, extracting the essential power from complex systems while eliminating unnecessary abstraction. It follows three key principles:

  1. Validation, Not Transformation - Sinter validates data structure and constraints but does not perform business logic transformations
  2. Runtime-First Design - Schemas are data structures that can be created and modified at runtime, perfect for dynamic frameworks
  3. Unified Core Engine - All validation flows through a single, well-tested pipeline for consistency and reliability

Summary

Functions

Creates a batch validator function for multiple type specifications.

Creates a schema by analyzing examples to infer field types and requirements.

Merges multiple schemas into a single schema.

Validates multiple values efficiently against their respective type specifications.

Validates a single value against a type specification.

Validates a single named value against a type specification.

Creates a reusable validation function for a specific type and constraints.

Types

schema()

@type schema() :: Sinter.Schema.t()

validation_opts()

@type validation_opts() :: Sinter.Validator.validation_opts()

validation_result()

@type validation_result() :: {:ok, term()} | {:error, [Sinter.Error.t()]}

Functions

batch_validator_for(field_specs, base_opts \\ [])

@spec batch_validator_for([{atom(), Sinter.Types.type_spec()}], validation_opts()) ::
  (map() ->
     {:ok, map()}
     | {:error,
        [
          Sinter.Error.t()
        ]})

Creates a batch validator function for multiple type specifications.

Parameters

  • field_specs - List of field specifications or {name, type_spec} tuples
  • base_opts - Base validation options

Examples

iex> batch_validator = Sinter.batch_validator_for([
...>   {:name, :string},
...>   {:age, :integer}
...> ])
iex> batch_validator.(%{name: "Alice", age: 30})
{:ok, %{name: "Alice", age: 30}}

infer_schema(examples, opts \\ [])

@spec infer_schema(
  [map()],
  keyword()
) :: Sinter.Schema.t()

Creates a schema by analyzing examples to infer field types and requirements.

This function is essential for DSPEx teleprompters like MIPRO that need to dynamically optimize schemas based on program execution examples.

Parameters

  • examples - List of maps representing example data
  • opts - Schema creation options

Options

  • :title - Schema title
  • :description - Schema description
  • :strict - Whether to reject unknown fields (default: false)
  • :min_occurrence_ratio - Minimum ratio for field to be considered required (default: 0.8)

Returns

  • Schema.t() - A schema inferred from the examples

Examples

iex> examples = [
...>   %{"name" => "Alice", "age" => 30},
...>   %{"name" => "Bob", "age" => 25},
...>   %{"name" => "Charlie", "age" => 35}
...> ]
iex> schema = Sinter.infer_schema(examples)
iex> fields = Sinter.Schema.fields(schema)
iex> fields[:name].type
:string
iex> fields[:age].type
:integer

Algorithm

  1. Field Discovery: Find all unique field names across examples
  2. Type Inference: Determine the most common type for each field
  3. Requirement Analysis: Fields present in >= min_occurrence_ratio are required
  4. Constraint Inference: Infer basic constraints from value patterns

merge_schemas(schemas, opts \\ [])

@spec merge_schemas(
  [Sinter.Schema.t()],
  keyword()
) :: Sinter.Schema.t()

Merges multiple schemas into a single schema.

This is useful for DSPEx signature composition where you need to combine input and output schemas or merge component signatures.

Parameters

  • schemas - List of Schema.t() to merge
  • opts - Optional schema configuration overrides

Merge Rules

  1. Fields: All fields from all schemas are included
  2. Conflicts: Later schemas override earlier ones for conflicting field definitions
  3. Configuration: First non-nil configuration value wins, except for :strict where last wins

Examples

iex> input_schema = Sinter.Schema.define([
...>   {:query, :string, [required: true]}
...> ])
iex> output_schema = Sinter.Schema.define([
...>   {:answer, :string, [required: true]},
...>   {:confidence, :float, [optional: true]}
...> ])
iex> merged = Sinter.merge_schemas([input_schema, output_schema])
iex> fields = Sinter.Schema.fields(merged)
iex> Map.keys(fields)
[:query, :answer, :confidence]

validate_many(type_value_pairs, opts \\ [])

@spec validate_many(
  [
    {Sinter.Types.type_spec(), term()}
    | {atom(), Sinter.Types.type_spec(), term()}
    | {atom(), Sinter.Types.type_spec(), term(), keyword()}
  ],
  validation_opts()
) :: {:ok, [term()]} | {:error, %{required(integer()) => [Sinter.Error.t()]}}

Validates multiple values efficiently against their respective type specifications.

Parameters

  • type_value_pairs - List of {type_spec, value} or {field_name, type_spec, value} tuples
  • opts - Validation options

Examples

iex> Sinter.validate_many([
...>   {:string, "hello"},
...>   {:integer, 42},
...>   {:email, :string, "test@example.com", [format: ~r/@/]}
...> ])
{:ok, ["hello", 42, "test@example.com"]}

validate_type(type_spec, value, opts \\ [])

@spec validate_type(Sinter.Types.type_spec(), term(), validation_opts()) ::
  validation_result()

Validates a single value against a type specification.

This is a convenient helper for one-off type validation that creates a temporary schema internally and uses the unified validation engine.

Parameters

  • type_spec - The type specification to validate against
  • value - The value to validate
  • opts - Validation options

Options

  • :coerce - Enable type coercion (default: false)
  • :constraints - Additional constraints to apply

Returns

  • {:ok, validated_value} on success
  • {:error, errors} on validation failure

Examples

iex> Sinter.validate_type(:integer, "42", coerce: true)
{:ok, 42}

iex> Sinter.validate_type({:array, :string}, ["hello", "world"])
{:ok, ["hello", "world"]}

iex> {:error, [error]} = Sinter.validate_type(:string, 123)
iex> error.code
:type

validate_value(field_name, type_spec, value, opts \\ [])

@spec validate_value(atom(), Sinter.Types.type_spec(), term(), validation_opts()) ::
  validation_result()

Validates a single named value against a type specification.

This is a convenient helper for single field validation that creates a temporary schema internally.

Parameters

  • field_name - Name for the field (used in error messages)
  • type_spec - The type specification to validate against
  • value - The value to validate
  • opts - Validation options and constraints

Examples

iex> Sinter.validate_value(:email, :string, "test@example.com",
...>   constraints: [format: ~r/@/])
{:ok, "test@example.com"}

iex> Sinter.validate_value(:score, :integer, "95",
...>   coerce: true, constraints: [gteq: 0, lteq: 100])
{:ok, 95}

validator_for(type_spec, base_opts \\ [])

@spec validator_for(Sinter.Types.type_spec(), validation_opts()) :: (term() ->
                                                                 validation_result())

Creates a reusable validation function for a specific type and constraints.

Returns a function that can be used to validate multiple values against the same specification efficiently.

Parameters

  • type_spec - The type specification
  • base_opts - Base validation options and constraints

Examples

iex> email_validator = Sinter.validator_for(:string,
...>   constraints: [format: ~r/@/])
iex> email_validator.("test@example.com")
{:ok, "test@example.com"}
iex> {:error, [error]} = email_validator.("invalid")
iex> error.code
:format