discourse/lib/service/base/steps_helpers.rb
Loïc Guitaut 3ed866a6c9
DEV: Add each step to service framework for collection iteration (#38759)
Services that process collections (bulk delete, bulk create, etc.)
currently require manual iteration inside a `step`, losing the
framework's built-in error handling and inspection. This adds a
first-class `each` step that brings the full service DSL inside the
iteration loop.

```ruby
  each :users do
    policy :can_delete
    step :destroy
  end
```

Each iteration receives the singularized item name (`user:`) and
`index:` as keyword arguments. If any nested step fails, iteration stops
and the failing item/index remain in context for error reporting.
Existing matchers (`fail_a_policy`, `fail_a_step`, etc.) work inside
each blocks.

A `persist:` option allows values to accumulate across iterations and
survive the loop's variable isolation:

```ruby
  each :tag_names, persist: { results: -> { { created: [], failed: [] } } } do
    step :create_tag
  end
  # context[:results] available after the loop
```

The steps inspector displays iteration progress ((3/3) on success, (1/3)
when failing at the first item, (empty collection) with ⏭️ when
skipped).
2026-04-03 09:44:03 +02:00

58 lines
1.6 KiB
Ruby

# frozen_string_literal: true
module Service
module Base
# Internal module to define available steps as DSL
# @!visibility private
module StepsHelpers
def model(name = :model, step_name = :"fetch_#{name}", optional: false)
steps << ModelStep.new(name, step_name, optional:)
end
def params(
name = :default,
default_values_from: nil,
base_class: Service::ContractBase,
&block
)
contract_class = Class.new(base_class).tap { it.class_eval(&block) if block }
const_set("#{name.to_s.classify.sub("Default", "")}Contract", contract_class)
steps << ContractStep.new(name, class_name: contract_class, default_values_from:)
end
def policy(name = :default, class_name: nil)
steps << PolicyStep.new(name, class_name:)
end
def step(name)
steps << Step.new(name)
end
def transaction(&block)
steps << TransactionStep.new(&block)
end
def lock(*keys, &block)
steps << LockStep.new(*keys, &block)
end
def options(&block)
klass = Class.new(Service::OptionsBase).tap { it.class_eval(&block) }
const_set("Options", klass)
steps << OptionsStep.new(:default, class_name: klass)
end
def try(*exceptions, &block)
steps << TryStep.new(exceptions, &block)
end
def only_if(name, &block)
steps << OnlyIfStep.new(name, &block)
end
def each(collection_name, as: nil, persist: nil, &block)
steps << EachStep.new(collection_name, as:, persist:, &block)
end
end
end
end