# 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