# 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, # typically provided by a user calling an endpoint. A special embedded # +Contract+ class has to be defined to holds 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. # * +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 # # 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 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