mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-04 06:55:00 +08:00
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).
94 lines
2.2 KiB
Ruby
94 lines
2.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Service
|
|
module Base
|
|
# Simple structure to hold the context of the service during its whole lifecycle.
|
|
class Context
|
|
delegate :slice, :dig, :merge!, to: :store
|
|
|
|
def self.build(context = {})
|
|
self === context ? context : new(context)
|
|
end
|
|
|
|
def initialize(context = {})
|
|
@store = context.symbolize_keys
|
|
end
|
|
|
|
def [](key)
|
|
store[key.to_sym]
|
|
end
|
|
|
|
def []=(key, value)
|
|
store[key.to_sym] = value
|
|
end
|
|
|
|
def to_h
|
|
store.deep_dup
|
|
end
|
|
|
|
def with_isolation(persist_keys: [])
|
|
@isolated_store = to_h
|
|
yield
|
|
ensure
|
|
@store.merge!(@isolated_store.slice(*persist_keys, *step_result_keys))
|
|
@isolated_store = nil
|
|
end
|
|
|
|
# @return [Boolean] returns +true+ if the context is set as successful (default)
|
|
def success?
|
|
!failure?
|
|
end
|
|
|
|
# @return [Boolean] returns +true+ if the context is set as failed
|
|
# @see #fail!
|
|
# @see #fail
|
|
def failure?
|
|
@failure || false
|
|
end
|
|
|
|
# Marks the context as failed.
|
|
# @param context [Hash, Context] the context to merge into the current one
|
|
# @example
|
|
# context.fail!("failure": "something went wrong")
|
|
# @return [Context]
|
|
def fail!(context = {})
|
|
self.fail(context)
|
|
raise Failure, self
|
|
end
|
|
|
|
# Marks the context as failed without raising an exception.
|
|
# @param context [Hash, Context] the context to merge into the current one
|
|
# @example
|
|
# context.fail("failure": "something went wrong")
|
|
# @return [Context]
|
|
def fail(context = {})
|
|
store.merge!(context.symbolize_keys)
|
|
@failure = true
|
|
self
|
|
end
|
|
|
|
def inspect_steps
|
|
Service::StepsInspector.new(self).inspect
|
|
end
|
|
|
|
private
|
|
|
|
def store
|
|
@isolated_store || @store
|
|
end
|
|
|
|
def step_result_keys
|
|
store.keys.select { it.start_with?("result.") }
|
|
end
|
|
|
|
def method_missing(method_name, *args, &block)
|
|
return super if args.present?
|
|
store[method_name]
|
|
end
|
|
|
|
def respond_to_missing?(name, include_all)
|
|
store.key?(name) || super
|
|
end
|
|
end
|
|
end
|
|
end
|