2023-03-17 21:24:38 +08:00
|
|
|
|
# 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:
|
|
|
|
|
#
|
|
|
|
|
# * +contract(name = :default)+: used to validate the input parameters,
|
DEV: Have `contract` take a block in services
Currently in services, the `contract` step is only used to define where
the contract will be called in the execution flow. Then, a `Contract`
class has to be defined with validations in it.
This patch allows the `contract` step to take a block containing
validations, attributes, etc. directly. No need to then open a
`Contract` class later in the service.
It also has a nice side effect, as it’s now easy to define multiples
contracts inside the same service. Before, we had the `class_name:`
option, but it wasn’t really useful as you had to redefine a complete
new contract class.
Now, when using a name for the contract other than `default`, a new
contract will be created automatically using the provided name.
Example:
```ruby
contract(:user) do
attribute :user_id, :integer
validates :user_id, presence: true
end
```
This will create a `UserContract` class and use it, also putting the
resulting contract in `context[:user_contract]`.
2024-10-01 23:17:14 +08:00
|
|
|
|
# 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[:contract]+. When calling +step(name)+ or +model(name = :model)+
|
|
|
|
|
# methods after validating a contract, the contract should be used as an
|
|
|
|
|
# argument instead of context attributes.
|
2023-03-17 21:24:38 +08:00
|
|
|
|
# * +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 Base
|
|
|
|
|
#
|
|
|
|
|
# model :channel, :fetch_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
|
|
|
|
|
#
|
2024-03-07 22:44:12 +08:00
|
|
|
|
# def fetch_channel(channel_id:)
|
2023-03-17 21:24:38 +08:00
|
|
|
|
# Chat::Channel.find_by(id: channel_id)
|
|
|
|
|
# end
|
|
|
|
|
#
|
2024-03-07 22:44:12 +08:00
|
|
|
|
# def invalid_access(guardian:, channel:)
|
2023-03-17 21:24:38 +08:00
|
|
|
|
# guardian.can_preview_chat_channel?(channel) && guardian.can_delete_chat_channel?
|
|
|
|
|
# end
|
|
|
|
|
#
|
2024-03-07 22:44:12 +08:00
|
|
|
|
# def prevents_slug_collision(channel:)
|
2023-03-17 21:24:38 +08:00
|
|
|
|
# …
|
|
|
|
|
# end
|
|
|
|
|
#
|
2024-03-07 22:44:12 +08:00
|
|
|
|
# def soft_delete_channel(guardian:, channel:)
|
2023-03-17 21:24:38 +08:00
|
|
|
|
# …
|
|
|
|
|
# end
|
|
|
|
|
#
|
2024-03-07 22:44:12 +08:00
|
|
|
|
# def log_channel_deletion(guardian:, channel:)
|
2023-03-17 21:24:38 +08:00
|
|
|
|
# …
|
|
|
|
|
# end
|
|
|
|
|
#
|
2024-03-07 22:44:12 +08:00
|
|
|
|
# def enqueue_delete_channel_relations_job(channel:)
|
2023-03-17 21:24:38 +08:00
|
|
|
|
# …
|
|
|
|
|
# end
|
|
|
|
|
# end
|
|
|
|
|
# @example An example from the {UpdateChannelStatus} service which uses a contract
|
|
|
|
|
# class UpdateChannelStatus
|
|
|
|
|
# include Base
|
|
|
|
|
#
|
|
|
|
|
# model :channel, :fetch_channel
|
|
|
|
|
# contract
|
|
|
|
|
# policy :check_channel_permission
|
|
|
|
|
# step :change_status
|
|
|
|
|
#
|
|
|
|
|
# class Contract
|
|
|
|
|
# attribute :status
|
|
|
|
|
# validates :status, inclusion: { in: Chat::Channel.editable_statuses.keys }
|
|
|
|
|
# end
|
|
|
|
|
#
|
|
|
|
|
# …
|
|
|
|
|
# end
|
|
|
|
|
end
|