discourse/lib/service.rb
Loïc Guitaut 6759ad71ca
DEV: Extract step classes from Service::Base into individual files (#37956)
The `base.rb` file (640+ lines) contained 9 step classes, the `Context`
class, and the `StepsHelpers` DSL module alongside the core concern.
Each is now in its own file under `lib/service/base/`, following the
existing `lib/service.rb` + `lib/service/*.rb` pattern.

Also:
- Update stale inline documentation in `lib/service.rb`
- Use `NotImplementedError` instead of generic raise in `PolicyBase` and
`ActionBase`
- Move YARD DSL docs to a `@!parse` block at the top of `base.rb`
2026-02-23 09:49:37 +01:00

97 lines
3.7 KiB
Ruby
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# frozen_string_literal: true
module Service
# Module to be included to provide steps DSL to any class. This allows to
# create easy to understand services as the whole service cycle is visible
# simply by reading the beginning of its class.
#
# Steps are executed in the order theyre defined. They will use their name
# to execute the corresponding method defined in the service class.
#
# Available step types:
#
# * +params(name = :default)+: validates input parameters using
# +ActiveModel+ validations. Fails if the contract is invalid.
# The resulting contract is available in +context[:params]+.
# * +model(name = :model)+: fetches or instantiates a model. Fails if the
# return value is falsy, empty, or invalid. The result is stored in
# +context[name]+ (+context[:model]+ by default).
# * +policy(name = :default)+: performs a check on the state of the system.
# Typically used to run guardians. Fails if the return value is falsy.
# * +step(name)+: runs arbitrary code. Does not fail based on its return
# value; call {#fail!} explicitly to mark the service as failed.
# * +transaction+: wraps other steps inside a DB transaction. Any failing
# step inside the block causes a rollback.
# * +try(*exceptions)+: wraps other steps and catches specified exceptions
# (defaults to +StandardError+). Fails if an exception is caught.
# * +lock(*keys)+: wraps other steps inside a +DistributedMutex+. Keys are
# resolved from params then the service context. Fails if the lock
# cannot be acquired.
# * +options+: defines options to parameterize service behavior, similar to
# +params+ but without validations. Cannot fail.
# * +only_if(name)+: conditionally runs the steps in its block. If the
# condition is not met, the steps are skipped without failing.
#
# The methods defined on the service are automatically provided with
# the whole context passed as keyword arguments. This allows to define in a
# very explicit way what dependencies are used by the method. If for
# whatever reason a key isnt found in the current context, then Ruby will
# raise an exception when the method is called.
#
# Regarding contract classes, they automatically have {ActiveModel} modules
# included so all the {ActiveModel} API is available.
#
# @example An example from the {TrashChannel} service
# class TrashChannel
# include Service::Base
#
# model :channel
# policy :invalid_access
# transaction do
# step :prevents_slug_collision
# step :soft_delete_channel
# step :log_channel_deletion
# end
# step :enqueue_delete_channel_relations_job
#
# private
#
# def fetch_channel(channel_id:)
# Chat::Channel.find_by(id: channel_id)
# end
#
# def invalid_access(guardian:, channel:)
# guardian.can_preview_chat_channel?(channel) && guardian.can_delete_chat_channel?
# end
#
# def prevents_slug_collision(channel:)
# …
# end
#
# def soft_delete_channel(guardian:, channel:)
# …
# end
#
# def log_channel_deletion(guardian:, channel:)
# …
# end
#
# def enqueue_delete_channel_relations_job(channel:)
# …
# end
# end
# @example An example from the {UpdateChannelStatus} service which uses a contract
# class UpdateChannelStatus
# include Service::Base
#
# model :channel
# params do
# attribute :status
# validates :status, inclusion: { in: Chat::Channel.editable_statuses.keys }
# end
# policy :check_channel_permission
# step :change_status
#
# …
# end
end