discourse/documentation/chat/backend/Chat/Service/Base.html
Martin Brennan 60ad836313
DEV: Chat service object initial implementation (#19814)
This is a combined work of Martin Brennan, Loïc Guitaut, and Joffrey Jaffeux.

---

This commit implements a base service object when working in chat. The documentation is available at https://discourse.github.io/discourse/chat/backend/Chat/Service.html

Generating documentation has been made as part of this commit with a bigger goal in mind of generally making it easier to dive into the chat project.

Working with services generally involves 3 parts:

- The service object itself, which is a series of steps where few of them are specialized (model, transaction, policy)

```ruby
class UpdateAge
  include Chat::Service::Base

  model :user, :fetch_user
  policy :can_see_user
  contract
  step :update_age

  class Contract
    attribute :age, :integer
  end

  def fetch_user(user_id:, **)
    User.find_by(id: user_id)
  end

  def can_see_user(guardian:, **)
    guardian.can_see_user(user)
  end

  def update_age(age:, **)
    user.update!(age: age)
  end
end
```

- The `with_service` controller helper, handling success and failure of the service within a service and making easy to return proper response to it from the controller

```ruby
def update
  with_service(UpdateAge) do
    on_success { render_serialized(result.user, BasicUserSerializer, root: "user") }
  end
end
```

- Rspec matchers and steps inspector, improving the dev experience while creating specs for a service

```ruby
RSpec.describe(UpdateAge) do
  subject(:result) do
    described_class.call(guardian: guardian, user_id: user.id, age: age)
  end

  fab!(:user) { Fabricate(:user) }
  fab!(:current_user) { Fabricate(:admin) }

  let(:guardian) { Guardian.new(current_user) }
  let(:age) { 1 }

   it { expect(user.reload.age).to eq(age) }
end
```

Note in case of unexpected failure in your spec, the output will give all the relevant information:

```
  1) UpdateAge when no channel_id is given is expected to fail to find a model named 'user'
     Failure/Error: it { is_expected.to fail_to_find_a_model(:user) }

       Expected model 'foo' (key: 'result.model.user') was not found in the result object.

       [1/4] [model] 'user' 
       [2/4] [policy] 'can_see_user'
       [3/4] [contract] 'default'
       [4/4] [step] 'update_age'

       /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/update_age.rb:32:in `fetch_user': missing keyword: :user_id (ArgumentError)
       	from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/base.rb:202:in `instance_exec'
       	from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/base.rb:202:in `call'
       	from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/base.rb:219:in `call'
       	from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/base.rb:417:in `block in run!'
       	from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/base.rb:417:in `each'
       	from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/base.rb:417:in `run!'
       	from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/base.rb:411:in `run'
       	from <internal:kernel>:90:in `tap'
       	from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/base.rb:302:in `call'
       	from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/spec/services/update_age_spec.rb:15:in `block (3 levels) in <main>'
```
2023-02-13 13:09:57 +01:00

723 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
Module: Chat::Service::Base
&mdash; Documentation by YARD 0.9.28
</title>
<link rel="stylesheet" href="../../css/style.css" type="text/css" />
<link rel="stylesheet" href="../../css/common.css" type="text/css" />
<script type="text/javascript">
pathId = "Chat::Service::Base";
relpath = '../../';
</script>
<script type="text/javascript" charset="utf-8" src="../../js/jquery.js"></script>
<script type="text/javascript" charset="utf-8" src="../../js/app.js"></script>
</head>
<body>
<div class="nav_wrap">
<iframe id="nav" src="../../class_list.html?1"></iframe>
<div id="resizer"></div>
</div>
<div id="main" tabindex="-1">
<div id="header">
<div id="menu">
<a href="../../_index.html">Index (B)</a> &raquo;
<span class='title'><span class='object_link'><a href="../../Chat.html" title="Chat (module)">Chat</a></span></span> &raquo; <span class='title'><span class='object_link'><a href="../Service.html" title="Chat::Service (module)">Service</a></span></span>
&raquo;
<span class="title">Base</span>
</div>
<div id="search">
<a class="full_list_link" id="class_list_link"
href="../../class_list.html">
<svg width="24" height="24">
<rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
<rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
<rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
</svg>
</a>
</div>
<div class="clear"></div>
</div>
<div id="content"><h1>Module: Chat::Service::Base
</h1>
<div class="box_info">
<dl>
<dt>Extended by:</dt>
<dd>ActiveSupport::Concern</dd>
</dl>
<dl>
<dt>Included in:</dt>
<dd><span class='object_link'><a href="TrashChannel.html" title="Chat::Service::TrashChannel (class)">TrashChannel</a></span>, <span class='object_link'><a href="UpdateChannel.html" title="Chat::Service::UpdateChannel (class)">UpdateChannel</a></span>, <span class='object_link'><a href="UpdateChannelStatus.html" title="Chat::Service::UpdateChannelStatus (class)">UpdateChannelStatus</a></span>, <span class='object_link'><a href="UpdateUserLastRead.html" title="Chat::Service::UpdateUserLastRead (class)">UpdateUserLastRead</a></span></dd>
</dl>
<dl>
<dt>Defined in:</dt>
<dd>plugins/chat/app/services/base.rb</dd>
</dl>
</div>
<h2>Overview</h2><div class="docstring">
<div class="discussion">
<p>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.</p>
<p>Steps are executed in the order theyre defined. They will use their name to execute the corresponding method defined in the service class.</p>
<p>Currently, there are 5 types of steps:</p>
<ul><li>
<p><tt>model(name = :model)</tt>: 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 <code>context[name]</code> (<code>context[:model]</code> by default).</p>
</li><li>
<p><tt>policy(name = :default)</tt>: 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.</p>
</li><li>
<p><tt>contract(name = :default)</tt>: used to validate the input parameters, typically provided by a user calling an endpoint. A special embedded <code>Contract</code> 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 <tt><a href=":"contract.default"">context</a></tt>.</p>
</li><li>
<p><tt>step(name)</tt>: used to run small snippets of arbitrary code. The step doesnt care about its return value, so to mark the service as failed, #fail! has to be called explicitly.</p>
</li><li>
<p><code>transaction</code>: used to wrap other steps inside a DB transaction.</p>
</li></ul>
<p>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 isnt found in the current context, then Ruby will raise an exception when the method is called.</p>
<p>Regarding contract classes, they have automatically ActiveModel modules included so all the ActiveModel API is available.</p>
</div>
</div>
<div class="tags">
<div class="examples">
<p class="tag_title">Examples:</p>
<p class="example_title"><div class='inline'>
<p>An example from the <span class='object_link'><a href="TrashChannel.html" title="Chat::Service::TrashChannel (class)">TrashChannel</a></span> service</p>
</div></p>
<pre class="example code"><code><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="TrashChannel.html" title="Chat::Service::TrashChannel (class)">TrashChannel</a></span></span>
<span class='id identifier rubyid_include'>include</span> <span class='const'>Base</span>
<span class='id identifier rubyid_model'>model</span> <span class='symbol'>:channel</span><span class='comma'>,</span> <span class='symbol'>:fetch_channel</span>
<span class='id identifier rubyid_policy'>policy</span> <span class='symbol'>:invalid_access</span>
<span class='id identifier rubyid_transaction'>transaction</span> <span class='kw'>do</span>
<span class='id identifier rubyid_step'>step</span> <span class='symbol'>:prevents_slug_collision</span>
<span class='id identifier rubyid_step'>step</span> <span class='symbol'>:soft_delete_channel</span>
<span class='id identifier rubyid_step'>step</span> <span class='symbol'>:log_channel_deletion</span>
<span class='kw'>end</span>
<span class='id identifier rubyid_step'>step</span> <span class='symbol'>:enqueue_delete_channel_relations_job</span>
<span class='id identifier rubyid_private'>private</span>
<span class='kw'>def</span> <span class='id identifier rubyid_fetch_channel'>fetch_channel</span><span class='lparen'>(</span><span class='label'>channel_id:</span><span class='comma'>,</span> <span class='op'>**</span><span class='rparen'>)</span>
<span class='const'>ChatChannel</span><span class='period'>.</span><span class='id identifier rubyid_find_by'>find_by</span><span class='lparen'>(</span><span class='label'>id:</span> <span class='id identifier rubyid_channel_id'>channel_id</span><span class='rparen'>)</span>
<span class='kw'>end</span>
<span class='kw'>def</span> <span class='id identifier rubyid_invalid_access'>invalid_access</span><span class='lparen'>(</span><span class='label'>guardian:</span><span class='comma'>,</span> <span class='label'>channel:</span><span class='comma'>,</span> <span class='op'>**</span><span class='rparen'>)</span>
<span class='id identifier rubyid_guardian'>guardian</span><span class='period'>.</span><span class='id identifier rubyid_can_preview_chat_channel?'>can_preview_chat_channel?</span><span class='lparen'>(</span><span class='id identifier rubyid_channel'>channel</span><span class='rparen'>)</span> <span class='op'>&amp;&amp;</span> <span class='id identifier rubyid_guardian'>guardian</span><span class='period'>.</span><span class='id identifier rubyid_can_delete_chat_channel?'>can_delete_chat_channel?</span>
<span class='kw'>end</span>
<span class='kw'>def</span> <span class='id identifier rubyid_prevents_slug_collision'>prevents_slug_collision</span><span class='lparen'>(</span><span class='label'>channel:</span><span class='comma'>,</span> <span class='op'>**</span><span class='rparen'>)</span>
<span class='id identifier rubyid_…'></span>
<span class='kw'>end</span>
<span class='kw'>def</span> <span class='id identifier rubyid_soft_delete_channel'>soft_delete_channel</span><span class='lparen'>(</span><span class='label'>guardian:</span><span class='comma'>,</span> <span class='label'>channel:</span><span class='comma'>,</span> <span class='op'>**</span><span class='rparen'>)</span>
<span class='id identifier rubyid_…'></span>
<span class='kw'>end</span>
<span class='kw'>def</span> <span class='id identifier rubyid_log_channel_deletion'>log_channel_deletion</span><span class='lparen'>(</span><span class='label'>guardian:</span><span class='comma'>,</span> <span class='label'>channel:</span><span class='comma'>,</span> <span class='op'>**</span><span class='rparen'>)</span>
<span class='id identifier rubyid_…'></span>
<span class='kw'>end</span>
<span class='kw'>def</span> <span class='id identifier rubyid_enqueue_delete_channel_relations_job'>enqueue_delete_channel_relations_job</span><span class='lparen'>(</span><span class='label'>channel:</span><span class='comma'>,</span> <span class='op'>**</span><span class='rparen'>)</span>
<span class='id identifier rubyid_…'></span>
<span class='kw'>end</span>
<span class='kw'>end</span></code></pre>
<p class="example_title"><div class='inline'>
<p>An example from the <span class='object_link'><a href="UpdateChannelStatus.html" title="Chat::Service::UpdateChannelStatus (class)">UpdateChannelStatus</a></span> service which uses a contract</p>
</div></p>
<pre class="example code"><code><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="UpdateChannelStatus.html" title="Chat::Service::UpdateChannelStatus (class)">UpdateChannelStatus</a></span></span>
<span class='id identifier rubyid_include'>include</span> <span class='const'>Base</span>
<span class='id identifier rubyid_model'>model</span> <span class='symbol'>:channel</span><span class='comma'>,</span> <span class='symbol'>:fetch_channel</span>
<span class='id identifier rubyid_contract'>contract</span>
<span class='id identifier rubyid_policy'>policy</span> <span class='symbol'>:check_channel_permission</span>
<span class='id identifier rubyid_step'>step</span> <span class='symbol'>:change_status</span>
<span class='kw'>class</span> <span class='const'>Contract</span>
<span class='id identifier rubyid_attribute'>attribute</span> <span class='symbol'>:status</span>
<span class='id identifier rubyid_validates'>validates</span> <span class='symbol'>:status</span><span class='comma'>,</span> <span class='label'>inclusion:</span> <span class='lbrace'>{</span> <span class='label'>in:</span> <span class='const'>ChatChannel</span><span class='period'>.</span><span class='id identifier rubyid_editable_statuses'>editable_statuses</span><span class='period'>.</span><span class='id identifier rubyid_keys'>keys</span> <span class='rbrace'>}</span>
<span class='kw'>end</span>
<span class='id identifier rubyid_…'></span>
<span class='kw'>end</span></code></pre>
</div>
</div><h2>Defined Under Namespace</h2>
<p class="children">
<strong class="classes">Classes:</strong> <span class='object_link'><a href="Base/Context.html" title="Chat::Service::Base::Context (class)">Context</a></span>, <span class='object_link'><a href="Base/Failure.html" title="Chat::Service::Base::Failure (class)">Failure</a></span>
</p>
<h2>
Class Method Summary
<small><a href="#" class="summary_toggle">collapse</a></small>
</h2>
<ul class="summary">
<li class="public ">
<span class="summary_signature">
<a href="#contract-class_method" title="contract (class method)">.<strong>contract</strong>(name = :default, class_name: self::Contract, use_default_values_from: nil) &#x21d2; Object </a>
</span>
<span class="summary_desc"><div class='inline'>
<p>Checks the validity of the input parameters.</p>
</div></span>
</li>
<li class="public ">
<span class="summary_signature">
<a href="#model-class_method" title="model (class method)">.<strong>model</strong>(name = :model, step_name = :&quot;fetch_#{name}&quot;) &#x21d2; Object </a>
</span>
<span class="summary_desc"><div class='inline'>
<p>Evaluates arbitrary code to build or fetch a model (typically from the DB).</p>
</div></span>
</li>
<li class="public ">
<span class="summary_signature">
<a href="#policy-class_method" title="policy (class method)">.<strong>policy</strong>(name = :default) &#x21d2; Object </a>
</span>
<span class="summary_desc"><div class='inline'>
<p>Performs checks related to the state of the system.</p>
</div></span>
</li>
<li class="public ">
<span class="summary_signature">
<a href="#step-class_method" title="step (class method)">.<strong>step</strong>(name) &#x21d2; Object </a>
</span>
<span class="summary_desc"><div class='inline'>
<p>Runs arbitrary code.</p>
</div></span>
</li>
<li class="public ">
<span class="summary_signature">
<a href="#transaction-class_method" title="transaction (class method)">.<strong>transaction</strong>(&amp;block) &#x21d2; Object </a>
</span>
<span class="summary_desc"><div class='inline'>
<p>Runs steps inside a DB transaction.</p>
</div></span>
</li>
</ul>
<div id="class_method_details" class="method_details_list">
<h2>Class Method Details</h2>
<div class="method_details first">
<h3 class="signature first" id="contract-class_method">
.<strong>contract</strong>(name = :default, class_name: self::Contract, use_default_values_from: nil) &#x21d2; <tt>Object</tt>
</h3><div class="docstring">
<div class="discussion">
<p>Checks the validity of the input parameters. Implements ActiveModel::Validations and ActiveModel::Attributes.</p>
<p>It stores the resulting contract in <tt>context [“contract.default”]</tt> by default (can be customized by providing the <code>name</code> argument).</p>
</div>
</div>
<div class="tags">
<div class="examples">
<p class="tag_title">Examples:</p>
<pre class="example code"><code><span class='id identifier rubyid_contract'>contract</span>
<span class='kw'>class</span> <span class='const'>Contract</span>
<span class='id identifier rubyid_attribute'>attribute</span> <span class='symbol'>:name</span>
<span class='id identifier rubyid_validates'>validates</span> <span class='symbol'>:name</span><span class='comma'>,</span> <span class='label'>presence:</span> <span class='kw'>true</span>
<span class='kw'>end</span></code></pre>
</div>
<p class="tag_title">Parameters:</p>
<ul class="param">
<li>
<span class='name'>name</span>
<span class='type'>(<tt>Symbol</tt>)</span>
<em class="default">(defaults to: <tt>:default</tt>)</em>
&mdash;
<div class='inline'>
<p>name for this contract</p>
</div>
</li>
<li>
<span class='name'>class_name</span>
<span class='type'>(<tt>Class</tt>)</span>
<em class="default">(defaults to: <tt>self::Contract</tt>)</em>
&mdash;
<div class='inline'>
<p>a class defining the contract</p>
</div>
</li>
<li>
<span class='name'>use_default_values_from</span>
<span class='type'>(<tt>Symbol</tt>)</span>
<em class="default">(defaults to: <tt>nil</tt>)</em>
&mdash;
<div class='inline'>
<p>name of the model to get default values from</p>
</div>
</li>
</ul>
</div>
</div>
<div class="method_details ">
<h3 class="signature " id="model-class_method">
.<strong>model</strong>(name = :model, step_name = :&quot;fetch_#{name}&quot;) &#x21d2; <tt>Object</tt>
</h3><div class="docstring">
<div class="discussion">
<p>Evaluates arbitrary code to build or fetch a model (typically from the DB). If the step returns a falsy value, then the step will fail.</p>
<p>It stores the resulting model in <code>context[:model]</code> by default (can be customized by providing the <code>name</code> argument).</p>
</div>
</div>
<div class="tags">
<div class="examples">
<p class="tag_title">Examples:</p>
<pre class="example code"><code><span class='id identifier rubyid_model'>model</span> <span class='symbol'>:channel</span><span class='comma'>,</span> <span class='symbol'>:fetch_channel</span>
<span class='id identifier rubyid_private'>private</span>
<span class='kw'>def</span> <span class='id identifier rubyid_fetch_channel'>fetch_channel</span><span class='lparen'>(</span><span class='label'>channel_id:</span><span class='comma'>,</span> <span class='op'>**</span><span class='rparen'>)</span>
<span class='const'>ChatChannel</span><span class='period'>.</span><span class='id identifier rubyid_find_by'>find_by</span><span class='lparen'>(</span><span class='label'>id:</span> <span class='id identifier rubyid_channel_id'>channel_id</span><span class='rparen'>)</span>
<span class='kw'>end</span></code></pre>
</div>
<p class="tag_title">Parameters:</p>
<ul class="param">
<li>
<span class='name'>name</span>
<span class='type'>(<tt>Symbol</tt>)</span>
<em class="default">(defaults to: <tt>:model</tt>)</em>
&mdash;
<div class='inline'>
<p>name of the model</p>
</div>
</li>
<li>
<span class='name'>step_name</span>
<span class='type'>(<tt>Symbol</tt>)</span>
<em class="default">(defaults to: <tt>:&quot;fetch_#{name}&quot;</tt>)</em>
&mdash;
<div class='inline'>
<p>name of the method to call for this step</p>
</div>
</li>
</ul>
</div>
</div>
<div class="method_details ">
<h3 class="signature " id="policy-class_method">
.<strong>policy</strong>(name = :default) &#x21d2; <tt>Object</tt>
</h3><div class="docstring">
<div class="discussion">
<p>Performs checks related to the state of the system. If the step doesnt return a truthy value, then the policy will fail.</p>
</div>
</div>
<div class="tags">
<div class="examples">
<p class="tag_title">Examples:</p>
<pre class="example code"><code><span class='id identifier rubyid_policy'>policy</span> <span class='symbol'>:no_direct_message_channel</span>
<span class='id identifier rubyid_private'>private</span>
<span class='kw'>def</span> <span class='id identifier rubyid_no_direct_message_channel'>no_direct_message_channel</span><span class='lparen'>(</span><span class='label'>channel:</span><span class='comma'>,</span> <span class='op'>**</span><span class='rparen'>)</span>
<span class='op'>!</span><span class='id identifier rubyid_channel'>channel</span><span class='period'>.</span><span class='id identifier rubyid_direct_message_channel?'>direct_message_channel?</span>
<span class='kw'>end</span></code></pre>
</div>
<p class="tag_title">Parameters:</p>
<ul class="param">
<li>
<span class='name'>name</span>
<span class='type'>(<tt>Symbol</tt>)</span>
<em class="default">(defaults to: <tt>:default</tt>)</em>
&mdash;
<div class='inline'>
<p>name for this policy</p>
</div>
</li>
</ul>
</div>
</div>
<div class="method_details ">
<h3 class="signature " id="step-class_method">
.<strong>step</strong>(name) &#x21d2; <tt>Object</tt>
</h3><div class="docstring">
<div class="discussion">
<p>Runs arbitrary code. To mark a step as failed, a call to #fail! needs to be made explicitly.</p>
</div>
</div>
<div class="tags">
<div class="examples">
<p class="tag_title">Examples:</p>
<pre class="example code"><code><span class='id identifier rubyid_step'>step</span> <span class='symbol'>:update_channel</span>
<span class='id identifier rubyid_private'>private</span>
<span class='kw'>def</span> <span class='id identifier rubyid_update_channel'>update_channel</span><span class='lparen'>(</span><span class='label'>channel:</span><span class='comma'>,</span> <span class='label'>params_to_edit:</span><span class='comma'>,</span> <span class='op'>**</span><span class='rparen'>)</span>
<span class='id identifier rubyid_channel'>channel</span><span class='period'>.</span><span class='id identifier rubyid_update!'>update!</span><span class='lparen'>(</span><span class='id identifier rubyid_params_to_edit'>params_to_edit</span><span class='rparen'>)</span>
<span class='kw'>end</span></code></pre>
<p class="example_title"><div class='inline'>
<p>using #fail! in a step</p>
</div></p>
<pre class="example code"><code><span class='id identifier rubyid_step'>step</span> <span class='symbol'>:save_channel</span>
<span class='id identifier rubyid_private'>private</span>
<span class='kw'>def</span> <span class='id identifier rubyid_save_channel'>save_channel</span><span class='lparen'>(</span><span class='label'>channel:</span><span class='comma'>,</span> <span class='op'>**</span><span class='rparen'>)</span>
<span class='id identifier rubyid_fail!'>fail!</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>something went wrong</span><span class='tstring_end'>&quot;</span></span><span class='rparen'>)</span> <span class='kw'>unless</span> <span class='id identifier rubyid_channel'>channel</span><span class='period'>.</span><span class='id identifier rubyid_save'>save</span>
<span class='kw'>end</span></code></pre>
</div>
<p class="tag_title">Parameters:</p>
<ul class="param">
<li>
<span class='name'>name</span>
<span class='type'>(<tt>Symbol</tt>)</span>
&mdash;
<div class='inline'>
<p>the name of this step</p>
</div>
</li>
</ul>
</div>
</div>
<div class="method_details ">
<h3 class="signature " id="transaction-class_method">
.<strong>transaction</strong>(&amp;block) &#x21d2; <tt>Object</tt>
</h3><div class="docstring">
<div class="discussion">
<p>Runs steps inside a DB transaction.</p>
</div>
</div>
<div class="tags">
<div class="examples">
<p class="tag_title">Examples:</p>
<pre class="example code"><code><span class='id identifier rubyid_transaction'>transaction</span> <span class='kw'>do</span>
<span class='id identifier rubyid_step'>step</span> <span class='symbol'>:prevents_slug_collision</span>
<span class='id identifier rubyid_step'>step</span> <span class='symbol'>:soft_delete_channel</span>
<span class='id identifier rubyid_step'>step</span> <span class='symbol'>:log_channel_deletion</span>
<span class='kw'>end</span></code></pre>
</div>
<p class="tag_title">Parameters:</p>
<ul class="param">
<li>
<span class='name'>block</span>
<span class='type'>(<tt>Proc</tt>)</span>
&mdash;
<div class='inline'>
<p>a block containing steps to be run inside a transaction</p>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
<div id="footer">
Generated by
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
0.9.28.
</div>
</div>
</body>
</html>