# Operandi

Operandi is a simple yet powerful way to organize business logic in Ruby applications. Build services that are easy to test, maintain, and understand.

[Get started with Quickstart](https://light-services.kodkod.me/introduction/quickstart)

## Features

* ✨ **Simple**: Define your service as a class with `arguments`, `steps`, and `outputs`
* 📦 **No runtime dependencies**: Works stand-alone without requiring external gems at runtime
* 🔄 **Transactions**: Automatically rollback database changes if any step fails
* 🧬 **Inheritance**: Inherit from other services to reuse logic seamlessly
* ⚠️ **Error Handling**: Collect errors from steps and handle them your way
* 🔗 **Context**: Run multiple services sequentially within the same context
* 🧪 **RSpec Matchers**: Built-in RSpec matchers for expressive service tests
* 🔍 **RuboCop Integration**: Custom cops to enforce best practices at lint time
* 🌐 **Framework Agnostic**: Compatible with Rails, Hanami, or any Ruby framework
* 🧩 **Modularity**: Isolate and test your services with ease
* 🔷 **Sorbet & Tapioca**: Full support for Sorbet type checking and Tapioca DSL generation
* ✅ **100% Test Coverage**: Thoroughly tested and reliable
* ⚔️ **Battle-Tested**: In production use since 2017

## Simple Example

```ruby
class GreetService < Operandi::Base
  # Arguments
  arg :name, type: String
  arg :age, type: Integer

  # Steps
  step :build_message
  step :send_message

  # Outputs
  output :message, type: String

  private

  def build_message
    self.message = "Hello, #{name}! You are #{age} years old."
  end

  def send_message
    # Send logic goes here
  end
end
```

## Advanced Example (with Sorbet types and conditions)

```ruby
class User::ResetPassword < Operandi::Base
  # Arguments with Sorbet types
  arg :user, type: User, optional: true
  arg :email, type: String, optional: true
  arg :send_email, type: T::Boolean, default: true
  arg :metadata, type: T::Hash[Symbol, String], default: {}
  arg :notify_channels, type: T::Array[Symbol], default: [:email]

  # Steps
  step :validate
  step :find_user, unless: :user?
  step :generate_reset_token
  step :save_reset_token
  step :send_reset_email, if: :send_email?

  # Outputs
  output :user, type: User
  output :reset_token, type: String
  output :notifications_sent, type: T::Array[Symbol]

  private

  def validate
    errors.add(:base, "user or email is required") if !user? && !email?
  end

  def find_user
    self.user = User.find_by("LOWER(email) = ?", email.downcase)
    errors.add(:email, "not found") unless user
  end

  def generate_reset_token
    self.reset_token = SecureRandom.hex(32)
  end

  def save_reset_token
    user.update!(
      reset_password_token: reset_token,
      reset_password_sent_at: Time.current,
    )
  rescue ActiveRecord::RecordInvalid => e
    errors.from_record(e.record)
  end

  def send_reset_email
    Mailer::SendEmail
      .with(self) # Call sub-service with the same context
      .run(template: :reset_password, user:, reset_token:)
  end
end
```

[Get started with Operandi](https://light-services.kodkod.me/introduction/quickstart)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://light-services.kodkod.me/introduction/readme.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
