Expectant

Gem Version

A Ruby DSL for defining validation schemas with type coercion, defaults, and fallbacks. Built on dry-validation and dry-types.

Installation

gem 'expectant'

Quick Start

class InvoiceService
  include Expectant::DSL

  expects :inputs

  input :customer_id, type: :integer
  input :amount, type: :float
  input :status, type: :string, default: "draft"
  input :description, type: :string, optional: true

  input_rule(:amount) do
    key.failure("must be positive") if value && value <= 0
  end

  def process(data)
    result = inputs.validate(data)

    if result.success?
      # Use validated data: result.to_h
    else
      # Handle errors: result.errors.to_h
    end
  end
end

Core Features

Types

expects :inputs

input :name, type: :string
input :age, type: :integer
input :price, type: :float
input :active, type: :boolean
input :date, type: :date
input :time, type: :datetime
input :data, type: :hash
input :tags, type: :array
input :user, type: User  # Custom class

Defaults and Optional Fields

# Optional (can be omitted)
input :description, type: :string, optional: true

# Static default
input :status, type: :string, default: "draft"

# Dynamic default (proc)
input :created_at, type: :datetime, default: -> { Time.now }

Fallbacks

Automatically substitute values when validation fails:

input :per_page, type: :integer, default: 25, fallback: 25

input_rule(:per_page) do
  key.failure("max 100") if value && value > 100
end

# If per_page: 500 is provided, validation succeeds with fallback value 25

Validation Rules

# Single field
input_rule(:email) do
  key.failure("invalid") unless value&.include?("@")
end

# Multiple fields
input_rule(:start_date, :end_date) do
  base.failure("start must be before end") if values[:start_date] > values[:end_date]
end

# Global rule
input_rule do
  base.failure("error") if some_condition
end

Context

input_rule(:order) do
  valid = context[:orderable_columns] || []
  key.failure("invalid column") unless valid.include?(value)
end

# Pass context when validating
inputs.validate(data, context: { orderable_columns: ["id", "name"] })

Multiple Schemas

expects :inputs
expects :outputs

input :data, type: :hash
output :result, type: :string

inputs.validate(input_data)
outputs.validate(output_data)

Configuration

Customize validator method names:

Expectant.configure do |config|
  config.validator_suffix = "validation"  # input_validation instead of input_rule
  config.validator_prefix = "validate"    # validate_input
end

Development

bundle install
bundle exec rspec

License

MIT License