discourse/lib/service.rb
Loïc Guitaut 584424594e DEV: Replace params by the contract object in services
This patch replaces the parameters provided to a service through
`params` by the contract object.

That way, it allows better consistency when accessing input params. For
example, if you have a service without a contract, to access a
parameter, you need to use `params[:my_parameter]`. But with a contract,
you do this through `contract.my_parameter`. Now, with this patch,
you’ll be able to access it through `params.my_parameter` or
`params[:my_parameter]`.

Some methods have been added to the contract object to better mimic a
Hash. That way, when accessing/using `params`, you don’t have to think
too much about it:
- `params.my_key` is also accessible through `params[:my_key]`.
- `params.my_key = value` can also be done through `params[:my_key] =
  value`.
- `#slice` and `#merge` are available.
- `#to_hash` has been implemented, so the contract object will be
  automatically cast as a hash by Ruby depending on the context. For
  example, with an AR model, you can do this: `user.update(**params)`.
2024-10-25 14:48:34 +02:00

93 lines
3.3 KiB
Ruby

# 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 they’re defined. They will use their name
# to execute the corresponding method defined in the service class.
#
# Currently, there are 5 types of steps:
#
# * +params(name = :default)+: used to validate the input parameters,
# typically provided by a user calling an endpoint. A block has to be
# defined to hold the validations. If the validations fail, the step will
# fail. Otherwise, the resulting contract will be available in
# +context[:params]+.
# * +model(name = :model)+: used to instantiate a model (either by building
# it or fetching it from the DB). If a falsy value is returned, then the
# step will fail. Otherwise the resulting object will be assigned in
# +context[name]+ (+context[:model]+ by default).
# * +policy(name = :default)+: used to perform a check on the state of the
# system. Typically used to run guardians. If a falsy value is returned,
# the step will fail.
# * +step(name)+: used to run small snippets of arbitrary code. The step
# doesn’t care about its return value, so to mark the service as failed,
# {#fail!} has to be called explicitly.
# * +transaction+: used to wrap other steps inside a DB transaction.
#
# 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 isn’t 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