> For the complete documentation index, see [llms.txt](https://light-services.kodkod.me/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://light-services.kodkod.me/deep-dive/rubocop.md).

# RuboCop Integration

Operandi provides custom RuboCop cops to help enforce best practices in your service definitions.

## Setup

Add this to your `.rubocop.yml`:

```yaml
require:
  - operandi/rubocop
```

## Available Cops

### Operandi/ArgumentTypeRequired

Ensures all `arg` declarations include a `type:` option.

```ruby
# bad
arg :user_id
arg :params, default: {}

# good
arg :user_id, type: Integer
arg :params, type: Hash, default: {}
```

### Operandi/OutputTypeRequired

Ensures all `output` declarations include a `type:` option.

```ruby
# bad
output :result
output :data, optional: true

# good
output :result, type: Hash
output :data, type: Hash, optional: true
```

### Operandi/StepMethodExists

Ensures all `step` declarations have a corresponding method defined.

```ruby
# bad
class MyService < ApplicationService
  step :validate
  step :process  # missing method

  private

  def validate; end
end

# good
class MyService < ApplicationService
  step :validate
  step :process

  private

  def validate; end
  def process; end
end
```

**Configuration:** Use `ExcludedSteps` for inherited steps:

```yaml
Operandi/StepMethodExists:
  ExcludedSteps:
    - initialize_entity
    - assign_attributes
```

### Operandi/ConditionMethodExists

Ensures symbol conditions (`:if`, `:unless`) have corresponding methods defined.

This cop automatically recognizes predicate methods generated by `arg` and `output` declarations (e.g., `arg :user` creates `user?`).

```ruby
# bad
class MyService < ApplicationService
  step :notify, if: :should_notify?  # missing method

  private

  def notify; end
end

# good - explicit method
class MyService < ApplicationService
  step :notify, if: :should_notify?

  private

  def notify; end
  def should_notify?; true; end
end

# good - predicate from arg/output
class MyService < ApplicationService
  arg :user, type: User, optional: true

  step :greet, if: :user?  # user? is auto-generated

  private

  def greet; end
end
```

**Configuration:** Use `ExcludedMethods` for inherited condition methods:

```yaml
Operandi/ConditionMethodExists:
  ExcludedMethods:
    - admin?
    - guest?
```

### Operandi/DslOrder

Enforces consistent ordering of DSL declarations: `config` → `arg` → `step` → `output`

```ruby
# bad
class MyService < ApplicationService
  step :process
  arg :name, type: String
  config raise_on_error: true
end

# good
class MyService < ApplicationService
  config raise_on_error: true

  arg :name, type: String

  step :process

  output :result, type: Hash
end
```

### Operandi/MissingPrivateKeyword

Ensures step methods are defined as private.

```ruby
# bad
class MyService < ApplicationService
  step :process

  def process  # should be private
    # implementation
  end
end

# good
class MyService < ApplicationService
  step :process

  private

  def process
    # implementation
  end
end
```

### Operandi/NoDirectInstantiation

Prevents direct instantiation of service classes with `.new`.

```ruby
# bad
UserService.new(name: "John")

# good
UserService.run(name: "John")
UserService.run!(name: "John")
UserService.call(name: "John")
```

**Configuration:** Customize the pattern for service class detection:

```yaml
Operandi/NoDirectInstantiation:
  ServicePattern: 'Service$'  # default: matches classes ending with "Service"
```

### Operandi/DeprecatedMethods

Detects deprecated `done!` and `done?` method calls and suggests using `stop!` and `stopped?` instead. Includes autocorrection.

```ruby
# bad
class MyService < ApplicationService
  step :process

  private

  def process
    done! if condition_met?
    return if done?
  end
end

# good
class MyService < ApplicationService
  step :process

  private

  def process
    stop! if condition_met?
    return if stopped?
  end
end
```

**Configuration:** Customize the pattern for service class detection:

```yaml
Operandi/DeprecatedMethods:
  ServicePattern: 'Service$'  # default: matches classes ending with "Service"
```

### Operandi/PreferFailMethod

Detects `errors.add(:base, "message")` calls and suggests using the `fail!("message")` helper instead. Includes autocorrection.

```ruby
# bad
class MyService < ApplicationService
  step :process

  private

  def process
    errors.add(:base, "user is required")
    errors.add(:base, "invalid input", rollback: false)
  end
end

# good
class MyService < ApplicationService
  step :process

  private

  def process
    fail!("user is required")
    fail!("invalid input", rollback: false)
  end
end
```

The cop only detects `errors.add(:base, ...)` calls. It does not flag `errors.add(:field_name, ...)` calls for specific fields, as those should not use `fail!`.

**Configuration:** Customize the base service classes to check:

```yaml
Operandi/PreferFailMethod:
  BaseServiceClasses:
    - ApplicationService
    - BaseCreator
```

## Configuration

Full configuration example:

```yaml
require:
  - operandi/rubocop

Operandi/ArgumentTypeRequired:
  Enabled: true

Operandi/OutputTypeRequired:
  Enabled: true

Operandi/StepMethodExists:
  Enabled: true
  ExcludedSteps: []

Operandi/ConditionMethodExists:
  Enabled: true
  ExcludedMethods: []

Operandi/DslOrder:
  Enabled: true

Operandi/MissingPrivateKeyword:
  Enabled: true

Operandi/NoDirectInstantiation:
  Enabled: true
  ServicePattern: 'Service$'

Operandi/DeprecatedMethods:
  Enabled: true
  ServicePattern: 'Service$'

Operandi/PreferFailMethod:
  Enabled: true
  BaseServiceClasses:
    - ApplicationService
```

To disable a cop for specific files:

```yaml
Operandi/ArgumentTypeRequired:
  Exclude:
    - 'spec/**/*'
    - 'test/**/*'
```

## What's Next?

Learn more about testing your services:

[Next: Testing](/deep-dive/testing.md)
