Project

dexkit

0.0
No release in over 3 years
A toolbelt of patterns for your Rails applications: Operation, Event, Form
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

Runtime

~> 1.9
~> 2.6
 Project Readme

Dexkit

Rails patterns toolbelt. Equip to gain +4 DEX.

Documentation

Operations

Service objects with typed properties, transactions, error handling, and more.

class CreateUser < Dex::Operation
  prop :name,  String
  prop :email, String

  success _Ref(User)
  error   :email_taken

  def perform
    error!(:email_taken) if User.exists?(email: email)

    user = User.create!(name: name, email: email)

    after_commit { WelcomeMailer.with(user: user).deliver_later }

    user
  end
end

user = CreateUser.call(name: "Alice", email: "alice@example.com")
user.name  # => "Alice"

What you get out of the box

Typed properties – powered by literal. Plain classes, ranges, unions, arrays, nilable, and model references with auto-find:

prop :amount,   _Integer(1..)
prop :currency, _Union("USD", "EUR", "GBP")
prop :user,     _Ref(User)           # accepts User instance or ID
prop? :note,    String               # optional (nil by default)

Structured errors with error!, assert!, and rescue_from:

user = assert!(:not_found) { User.find_by(id: user_id) }

rescue_from Stripe::CardError, as: :card_declined

Ok / Err – pattern match on operation outcomes with .safe.call:

case CreateUser.new(email: email).safe.call
in Ok(name:)
  puts "Welcome, #{name}!"
in Err(code: :email_taken)
  puts "Already registered"
end

Async execution via ActiveJob:

SendWelcomeEmail.new(user_id: 123).async(queue: "mailers").call

Transactions on by default, advisory locking, recording to database, callbacks, and a customizable pipeline – all composable, all optional.

Testing

First-class test helpers for Minitest:

class CreateUserTest < Minitest::Test
  testing CreateUser

  def test_creates_user
    assert_operation(name: "Alice", email: "alice@example.com")
  end

  def test_rejects_duplicate_email
    assert_operation_error(:email_taken, name: "Alice", email: "taken@example.com")
  end
end

Events

Typed, immutable event objects with publish/subscribe, async dispatch, and causality tracing.

class OrderPlaced < Dex::Event
  prop :order_id, Integer
  prop :total, BigDecimal
  prop? :coupon_code, String
end

class NotifyWarehouse < Dex::Event::Handler
  on OrderPlaced
  retries 3

  def perform
    WarehouseApi.notify(event.order_id)
  end
end

OrderPlaced.publish(order_id: 1, total: 99.99)

What you get out of the box

Zero-config pub/sub — define events and handlers, publish. No bus setup needed.

Async by default — handlers dispatched via ActiveJob. sync: true for inline.

Causality tracing — link events in chains with shared trace_id:

order_placed.trace do
  InventoryReserved.publish(order_id: 1)
end

Suppression, optional persistence, context capture, and retries with exponential backoff.

Testing

class CreateOrderTest < Minitest::Test
  include Dex::Event::TestHelpers

  def test_publishes_order_placed
    capture_events do
      CreateOrder.call(item_id: 1)
      assert_event_published(OrderPlaced, order_id: 1)
    end
  end
end

Forms

Form objects with typed attributes, normalization, nested forms, and Rails form builder compatibility.

class OnboardingForm < Dex::Form
  model User

  attribute :first_name, :string
  attribute :last_name, :string
  attribute :email, :string

  normalizes :email, with: -> { _1&.strip&.downcase.presence }

  validates :email, presence: true, uniqueness: true
  validates :first_name, :last_name, presence: true

  nested_one :address do
    attribute :street, :string
    attribute :city, :string
    validates :street, :city, presence: true
  end
end

form = OnboardingForm.new(email: "  ALICE@EXAMPLE.COM  ", first_name: "Alice", last_name: "Smith")
form.email  # => "alice@example.com"
form.valid?

What you get out of the box

ActiveModel attributes with type casting, normalization, and full Rails validation DSL.

Nested formsnested_one and nested_many with automatic Hash coercion, _destroy support, and error propagation:

nested_many :documents do
  attribute :document_type, :string
  attribute :document_number, :string
  validates :document_type, :document_number, presence: true
end

Rails form compatibility — works with form_with, fields_for, and nested attributes out of the box.

Uniqueness validation against the database, with scope, case-sensitivity, and current-record exclusion.

Multi-model forms — when a form spans User, Employee, and Address, define a .for convention method to map records and a #save method that delegates to a Dex::Operation:

def save
  return false unless valid?

  case operation.safe.call
  in Ok then true
  in Err => e then errors.add(:base, e.message) and false
  end
end

Queries

Declarative query objects for filtering and sorting ActiveRecord relations.

class UserSearch < Dex::Query
  scope { User.all }

  prop? :name,   String
  prop? :role,   _Array(String)
  prop? :age_min, Integer

  filter :name,    :contains
  filter :role,    :in
  filter :age_min, :gte, column: :age

  sort :name, :created_at, default: "-created_at"
end

users = UserSearch.call(name: "ali", role: %w[admin], sort: "name")

What you get out of the box

11 built-in filter strategies:eq, :not_eq, :contains, :starts_with, :ends_with, :gt, :gte, :lt, :lte, :in, :not_in. Custom blocks for complex logic.

Sorting with ascending/descending column sorts, custom sort blocks, and defaults.

from_params — HTTP boundary handling with automatic coercion, blank stripping, and invalid value fallback:

class UsersController < ApplicationController
  def index
    query = UserSearch.from_params(params, scope: policy_scope(User))
    @users = pagy(query.resolve)
  end
end

Form binding — works with form_with for search forms. Queries respond to model_name, param_key, persisted?, and to_params.

Scope injection — narrow the base scope at call time without modifying the query class.

Installation

gem "dexkit"

Documentation

Full documentation at dex.razorjack.net.

AI Coding Assistant Setup

Dexkit ships LLM-optimized guides. Copy them into your project so AI agents automatically know the API:

cp $(bundle show dexkit)/guides/llm/OPERATION.md app/operations/CLAUDE.md
cp $(bundle show dexkit)/guides/llm/EVENT.md app/event_handlers/CLAUDE.md
cp $(bundle show dexkit)/guides/llm/FORM.md app/forms/CLAUDE.md
cp $(bundle show dexkit)/guides/llm/QUERY.md app/queries/CLAUDE.md

License

MIT