# 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.
  #
  # 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 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
