<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Evil Martians</title>
    <description>The latest articles on DEV Community by Evil Martians (@evilmartians).</description>
    <link>https://dev.to/evilmartians</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F148%2F5e8f4bf1-df6c-4245-96ba-d0e96569f212.png</url>
      <title>DEV Community: Evil Martians</title>
      <link>https://dev.to/evilmartians</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/evilmartians"/>
    <language>en</language>
    <item>
      <title>Nano Stores in Angular: how to make the state management simpler</title>
      <dc:creator>Nina Torgunakova</dc:creator>
      <pubDate>Fri, 21 Apr 2023 15:05:15 +0000</pubDate>
      <link>https://dev.to/evilmartians/nano-stores-in-angular-how-to-make-the-state-management-simpler-38a1</link>
      <guid>https://dev.to/evilmartians/nano-stores-in-angular-how-to-make-the-state-management-simpler-38a1</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; Angular now has &lt;a href="https://github.com/nanostores/angular"&gt;integration&lt;/a&gt; for &lt;a href="https://github.com/nanostores/nanostores"&gt;Nano Stores&lt;/a&gt;, an open source state manager based on the idea of atomic tree-shakable stores and direct manipulation. It is very small (from 334 bytes), has zero dependencies, and promotes moving logic from components to stores. With such stores, you don't need to call the selector function for all components on every store change, which makes them noticeably fast.&lt;/p&gt;

&lt;p&gt;Here is a small example of how we can use Nano Stores in an Angular component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;atom&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nanostores&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NanostoresService&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nanostores/angular&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;switchMap&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;atom&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;John&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app-root&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p *ngIf="(currentUser$ | async) as user"&amp;gt;{{ user.name }}&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;AppComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;currentUser$&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nanostores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user$&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;nanostores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NanostoresService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How can the appearance of Nano Stores in Angular improve our developer experience with state management? Let's start from the beginning.&lt;/p&gt;

&lt;p&gt;Nowadays the concept of state management is crucial for client-side development: we need a reliable source of truth to manage data in our applications. &lt;/p&gt;

&lt;p&gt;And in Angular, there exist specific approaches to manage state. In this area, there are a lot of implementation details that we’ve all become accustomed to over the years.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do we have now?
&lt;/h2&gt;

&lt;p&gt;When developing projects with Angular, we usually manage data state in the services, not in components or modules. But, using this approach, when the number of features increases, the number of services also increases and management becomes increasingly complicated. Because of that, we need a convenient approach to handle this process, which should also be predictable in order to reduce the number of possible bugs.&lt;/p&gt;

&lt;p&gt;Angular developers have typically been used to implementing state management logic using &lt;a href="https://rxjs.dev/"&gt;RxJS&lt;/a&gt; or concepts like &lt;a href="https://ngrx.io/"&gt;NgRx&lt;/a&gt;/&lt;a href="https://ngxs.io/"&gt;NGXS&lt;/a&gt; inspired by Redux, because these were the most popular approaches. Let's try to write a simple application with state management using these instruments and understand if they are really convenient to use.&lt;/p&gt;

&lt;h3&gt;
  
  
  RxJS
&lt;/h3&gt;

&lt;p&gt;Inside Angular, we already have a built-in option: &lt;a href="https://rxjs.dev/"&gt;RxJS&lt;/a&gt; (Reactive Extensions for JavaScript), a library for reactive programming, remains the most popular solution to compose callback-based or asynchronous code. We can also handle changing the state of our project by using RxJS operators, and it is quite a common solution, and isn’t too bad for many cases.&lt;/p&gt;

&lt;p&gt;Let's build a familiar example: a TODO List application. We’ll consider three functions for the list of tasks: adding a new task, deleting an existing task, and toggling the status of the task from undone to done, or vice versa.&lt;/p&gt;

&lt;p&gt;Here’s our HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;#newTask&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"addTask(newTask.value)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Add&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"todo-list"&lt;/span&gt; &lt;span class="na"&gt;*ngIf=&lt;/span&gt;&lt;span class="s"&gt;"(tasks$ | async) as tasks"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;*ngFor=&lt;/span&gt;&lt;span class="s"&gt;"let task of tasks"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"toggleStatus(this.task.id)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Toggle Status&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;[ngClass]=&lt;/span&gt;&lt;span class="s"&gt;"{'done': this.task.isDone }"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{this.task.name}}&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"deleteTask(this.task.id)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Delete&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we can define a service with basic methods for state management (which we can extend in the future with a more detailed implementation):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;distinctUntilChanged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// This type will be passed when extending the StateService&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;StateService&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// This field stores the current state's value and emits it to the new subscribers&lt;/span&gt;
  &lt;span class="kr"&gt;protected&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="na"&gt;$&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Initializes the state$ with some initial value&lt;/span&gt;
  &lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Returns the current value from the state&lt;/span&gt;
  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Emits the new state value&lt;/span&gt;
  &lt;span class="nx"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;newState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;({...&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,...&lt;/span&gt;&lt;span class="nx"&gt;newState&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="cm"&gt;/**  Called when state$ emits a new value.
  Operator distinctUntilChanged() will skip emissions
  until the piece of state obtain a new value or object reference */&lt;/span&gt;
  &lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;mapFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;asObservable&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;mapFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="nx"&gt;distinctUntilChanged&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that there are no mentions of the actual TODO tasks in this part of the code. So, you can use this service later for new features that are not related to the TODO list.&lt;/p&gt;

&lt;p&gt;Now we can extend &lt;code&gt;StateService&lt;/code&gt; with a more practical service, &lt;code&gt;TodosStateService&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;TodosStateService&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;StateService&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/** In the constructor we call the constructor of StateService and pass the initial state (empty array).
   * Injected instance of service with method getTasks() expose an API to receive the tasks. */&lt;/span&gt;
  &lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TodoListService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getTasks&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;getTasks&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

 &lt;span class="c1"&gt;// Exposes the corresponding state data to subscribers&lt;/span&gt;
  &lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="na"&gt;$&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s the structure for a TODO list Task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;isDone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;todoList.component.ts&lt;/code&gt; we need to inject instances of two services: &lt;code&gt;TodosStateService&lt;/code&gt; and &lt;code&gt;TodoListService&lt;/code&gt;.&lt;br&gt;
&lt;code&gt;TodoListService&lt;/code&gt; is a service that calls API methods for receiving, adding, deleting, and changing tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;todosStateService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TodosStateService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TodoListService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we need to subscribe to &lt;code&gt;$todos&lt;/code&gt; in the &lt;code&gt;TodosStateService&lt;/code&gt;. We can do this by declaring the component field like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;tasks$&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todosStateService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todos$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But don't forget to unsubscribe from this subscription when the component has been destroyed to prevent possible memory leaks. (You can do this any way you prefer, for example &lt;a href="https://www.techiediaries.com/angular-13-tutorial/unsubscribe-observables-rxjs-takeuntil/"&gt;using RxJS operator takeUntil&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;Finally, let's implement actions with our tasks in the list: adding, deleting, and status toggling. I also suggest trying to implement this using the optimistic UI pattern: we’ll update the UI even before receiving a server response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;addTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isDone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todosStateService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todosStateService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getTasks&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;newTask&lt;/span&gt;&lt;span class="p"&gt;]});&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newTask&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;deleteTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todosStateService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todosStateService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getTasks&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deleteTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;toggleStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todosStateService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todosStateService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getTasks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({...&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isDone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isDone&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isDone&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toggleStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These functions will invoke the &lt;code&gt;setState&lt;/code&gt; method in the service that maintains the state of tasks.&lt;/p&gt;

&lt;p&gt;That's all for the RxJS approach; it’s scalable but the biggest drawback is the large number of routine tasks. We need to create an entirely new service for the whole app, then create a new one that extends it each time when we need a feature with new entities.&lt;/p&gt;

&lt;h3&gt;
  
  
  NgRx
&lt;/h3&gt;

&lt;p&gt;Let's next try to develop the same state management example with &lt;a href="https://ngrx.io/"&gt;NgRx&lt;/a&gt;, a very popular state management library for Angular. It was inspired by Redux and based on such concepts as Stores, Actions, Effects, and Reducers.&lt;/p&gt;

&lt;p&gt;First of all, we’ll create a new file with actions. In NgRx, Actions are unique events that happen throughout the application. For simplicity in this case, we can define actions for a single task (adding, deleting, toggling status), and also the action to receive all tasks from the server (we can do it also by &lt;a href="https://v7.ngrx.io/guide/effects"&gt;Effects&lt;/a&gt;, but let's stick with  the easiest approach for this small example):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ngrx/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LoadTasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Load Tasks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;[]}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Add&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Add&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Delete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Delete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ToggleStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Toggle&lt;/span&gt; &lt;span class="nx"&gt;Status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, props&amp;lt;{id: string;}&amp;gt;());
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we need the Reducer for the tasks that will manage the state of our tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createReducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ngrx/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;taskReducer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createReducer&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt;
  &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LoadTasks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;])),&lt;/span&gt;
  &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ToggleStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({...&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isDone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isDone&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isDone&lt;/span&gt; &lt;span class="p"&gt;}))),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Did you notice we already have a lot of boilerplate for simple things?)&lt;/p&gt;

&lt;p&gt;We need to add the &lt;code&gt;StoreModule.forRoot&lt;/code&gt; function in the imports array of &lt;code&gt;NgModule&lt;/code&gt; and pass the object that contains the &lt;code&gt;taskReducer&lt;/code&gt;. The &lt;code&gt;StoreModule.forRoot()&lt;/code&gt; method registers the providers needed to access the global store throughout the application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StoreModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ngrx/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nl"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;StoreModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;todoState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;taskReducer&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well, now we can create the component with tasks. We have exactly the same HTML that we had for RxJS example, but the code in &lt;code&gt;component.ts&lt;/code&gt; will be different.&lt;/p&gt;

&lt;p&gt;After creating the new component, we need to inject  the instance of &lt;code&gt;TodoListService&lt;/code&gt; (we already used this in the previous example), and the Store service to dispatch the task actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TodoListService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;todoState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we can subscribe to task changes in the &lt;code&gt;ngOnInit&lt;/code&gt; method to receive the tasks from the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ngOnInit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getTasks&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;todos$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep in mind that, in this case, we also probably want to unsubscribe from the subscription when the component has been destroyed.&lt;/p&gt;

&lt;p&gt;We’re nearly finished. Let's add a field to the component with the actual state of tasks. We need to select them from the store using the &lt;code&gt;select&lt;/code&gt; method we recently created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;tasks$&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todoState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With regards to Optimistic UI, all we need to do is to add the following code for the actions with tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;addTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isDone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;todos$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;todos$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;newTask&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newTask&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;deleteTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;todos$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todos$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deleteTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;toggleStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;todos$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todos$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({...&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isDone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isDone&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isDone&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toggleStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All in all, we have changes in four different files, &lt;code&gt;module.ts&lt;/code&gt;, &lt;code&gt;component.ts&lt;/code&gt;, &lt;code&gt;actions.ts&lt;/code&gt;, and &lt;code&gt;reducer.ts&lt;/code&gt; (and we haven't even used Effects for even more simplicity.) We already have a lot of repeating actions to build just the basic features, imagine the result if we needed to add extended ones.&lt;/p&gt;

&lt;p&gt;Other than that, &lt;a href="https://bundlephobia.com/package/@ngrx/store@8.4.0"&gt;NgRx is a heavy library&lt;/a&gt; for your bundle and we need to spend time to properly understand how all these concepts, like Actions, Reducers, Selectors, and so on, work.&lt;/p&gt;

&lt;p&gt;But we can try another option to manage the state and make our life simpler.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nano Stores
&lt;/h3&gt;

&lt;p&gt;Let's try the same example with the TODO List application to see how to use Nanostores in this simple case — and how convenient it can be.&lt;/p&gt;

&lt;p&gt;First of all, we need to provide injection tokens in our &lt;code&gt;NgModule&lt;/code&gt; to make the Nano Stores work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NANOSTORES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NanostoresService&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nanostores/angular&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;NgModule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NANOSTORES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;useClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NanostoresService&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we need to create a new atomic store to hold the tasks. The initial state for the tasks will be the empty array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;atom&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nanostores&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;todos$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;atom&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here we also can define functions that we will invoke to update the value in our store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;addTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newTask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;todos$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;todos$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;newTask&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deleteTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;todos$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todos$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toggleStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;todos$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todos$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({...&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isDone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isDone&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isDone&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
 &lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's time to create the component with tasks (the HTML is all the same as in the previous section). We will inject the instance of our usual &lt;code&gt;TodoListService&lt;/code&gt;, and also the instance of &lt;code&gt;NanostoresService&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TodoListService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;nanostores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NanostoresService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can add a field into the component with the actual state of tasks using the &lt;code&gt;useStore&lt;/code&gt; method in the injected instance of &lt;code&gt;NanostoresService&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;tasks$&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nanostores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todos$&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then, in &lt;code&gt;ngOnInit&lt;/code&gt;, we can subscribe to the task changes method to receive the tasks from the server side and set them to the store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ngOnInit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getTasks&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;todos$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All that is left to do is to write the part of the component where we’ll call the functions that interact with the store (which we already wrote), and invoke the API methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;addTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isDone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;addTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newTask&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newTask&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;deleteTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;deleteTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deleteTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;toggleStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;toggleStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toggleStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's all. No additional files, utility services, and no boilerplate. Don't forget to unsubscribe from the initial subscription, as was described in previous sections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is Nanostores the handiest way to manage state?
&lt;/h2&gt;

&lt;p&gt;After solving this simple task with three different approaches and seeing the brevity and beauty of Nanostores, let's sum up the results. Why is Nanostores the best option to manage the state of an application in Angular?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It was designed to move logic from components to stores – less logic in components, clearer and more predictable development process&lt;/li&gt;
&lt;li&gt;No boilerplate, short and clear syntax&lt;/li&gt;
&lt;li&gt;It’s much smaller than NgRx and its analogs. For instance, even the base NgRx package  &lt;a href="https://bundlephobia.com/package/@ngrx/store@15.4.0"&gt;@ngrx/store&lt;/a&gt;, has a minified bundle size &lt;strong&gt;3 times more&lt;/strong&gt; than &lt;a href="https://bundlephobia.com/package/nanostores@0.8.0"&gt;nanostores&lt;/a&gt; and its &lt;a href="https://bundlephobia.com/package/@nanostores/angular@0.0.4"&gt;integration&lt;/a&gt; combined (using the &lt;a href="https://bundlephobia.com/"&gt;bundlephobia&lt;/a&gt; metric)
No complex concepts like Reducers, Effects, or Selectors, which must be deeply understood before using. You just need to deal with small atomic stores&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/nanostores/nanostores"&gt;Nanostores&lt;/a&gt; has a growing community and its own developing ecosystem: we already have a tiny &lt;a href="https://github.com/nanostores/router"&gt;router&lt;/a&gt; for stores, a &lt;a href="https://github.com/nanostores/persistent"&gt;persistent store&lt;/a&gt;, and a tiny &lt;a href="https://github.com/nanostores/query"&gt;data fetcher&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I believe that with open source projects like this, Angular can soon get rid of boilerplate, complex definitions, and huge packages. Try using &lt;a href="https://evilmartians.com/opensource/nano-stores"&gt;Nanostores&lt;/a&gt; in your Angular project to check out all these advantages for yourself.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Climbing Steep hills, or adopting Ruby 3 types</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Fri, 11 Dec 2020 07:53:55 +0000</pubDate>
      <link>https://dev.to/evilmartians/climbing-steep-hills-or-adopting-ruby-3-types-181a</link>
      <guid>https://dev.to/evilmartians/climbing-steep-hills-or-adopting-ruby-3-types-181a</guid>
      <description>&lt;p&gt;With Ruby 3.0 just around the corner, let's take a look at one of the highlights of the upcoming release: &lt;a href="https://github.com/ruby/rbs"&gt;Ruby Type Signatures&lt;/a&gt;. Yes, types come to our favourite dynamic language—let's see what could work out of that!&lt;/p&gt;

&lt;p&gt;It is not the first time I'm writing about types for Ruby: more than a year ago, I &lt;em&gt;tasted&lt;/em&gt; &lt;a href="https://sorbet.org"&gt;Sorbet&lt;/a&gt; and shared my experience in the &lt;a href="https://evilmartians.com/chronicles/sorbetting-a-gem"&gt;Martian Chronicles&lt;/a&gt;. At the end of the post, I promised to give another Ruby type checker a try: &lt;a href="https://github.com/soutaro/steep"&gt;Steep&lt;/a&gt;. So, here I am, paying my debts!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'd highly recommend taking a look at the &lt;a href="https://evilmartians.com/chronicles/sorbetting-a-gem"&gt;"Sorbetting a gem" post&lt;/a&gt; first since I will refer to it multiple times today.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  RBS in a nutshell
&lt;/h2&gt;

&lt;p&gt;RBS is a language to describe &lt;em&gt;the structure of Ruby programs&lt;/em&gt; (from &lt;a href="https://github.com/ruby/rbs"&gt;Readme&lt;/a&gt;). The "structure" includes class and method signatures, type definitions, etc.&lt;/p&gt;

&lt;p&gt;Since it's a separate language, not Ruby, separate &lt;code&gt;.rbs&lt;/code&gt; files are used to store typings.&lt;/p&gt;

&lt;p&gt;Let's jump right into an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# martian.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Martian&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Alien&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;evil: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@evil&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evil&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evil?&lt;/span&gt;
    &lt;span class="vi"&gt;@evil&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# martian.rbs&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Alien&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Martian&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Alien&lt;/span&gt;
  &lt;span class="vi"&gt;@evil&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="ss"&gt;evil: &lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evil?&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature looks pretty similar to the class definition itself, except that we have types specified for arguments, methods, and instance variables. So far, looks pretty Ruby-ish. However, RBS has some entities which are missing in Ruby, for example, &lt;em&gt;interfaces&lt;/em&gt;. We're gonna see some examples later.&lt;/p&gt;

&lt;p&gt;RBS itself doesn't provide any functionality to perform type checking*; it's just a language, remember? That's where Steep comes into a stage.&lt;/p&gt;

&lt;p&gt;* Actually, that's not 100% true; there is runtime type checking mode. Continue reading to learn more.&lt;/p&gt;

&lt;p&gt;In the rest of the article, I will describe the process of adding RBS and Steep to &lt;a href="https://github.com/palkan/rubanok"&gt;Rubanok&lt;/a&gt; (the same project as I used in the Sorbet example, though the more recent version).&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started with RBS
&lt;/h2&gt;

&lt;p&gt;It could be hard to figure out how to start adding types to an existing project. Hopefully, RBS provides a way to generate a types scaffold for your code.&lt;/p&gt;

&lt;p&gt;RBS comes with a CLI tool (&lt;code&gt;rbs&lt;/code&gt;) which has a bunch of commands, but we're interested only in the &lt;code&gt;prototype&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;rbs prototype &lt;span class="nt"&gt;-h&lt;/span&gt;
Usage: rbs prototype &lt;span class="o"&gt;[&lt;/span&gt;generator...] &lt;span class="o"&gt;[&lt;/span&gt;args...]

Generate prototype of RBS files.
Supported generators are rb, rbi, runtime.

Examples:

  &lt;span class="nv"&gt;$ &lt;/span&gt;rbs prototype rb foo.rb
  &lt;span class="nv"&gt;$ &lt;/span&gt;rbs prototype rbi foo.rbi
  &lt;span class="nv"&gt;$ &lt;/span&gt;rbs prototype runtime String
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The description is pretty self-explanatory; let's try it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;rbs prototype rb lib/&lt;span class="k"&gt;**&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.rb

&lt;span class="c"&gt;# Rubanok provides a DSL ... (all the comments from the source file) &lt;/span&gt;
module Rubanok
  attr_accessor ignore_empty_values: untyped
  attr_accessor fail_when_no_matches: untyped
end

module Rubanok
  class Rule
    &lt;span class="c"&gt;# :nodoc:&lt;/span&gt;
    UNDEFINED: untyped

    attr_reader fields: untyped
    attr_reader activate_on: untyped
    attr_reader activate_always: untyped
    attr_reader ignore_empty_values: untyped
    attr_reader filter_with: untyped

    def initialize: &lt;span class="o"&gt;(&lt;/span&gt;untyped fields, ?activate_on: untyped activate_on, ?activate_always: bool activate_always, ?ignore_empty_values: untyped ignore_empty_values, ?filter_with: untyped? filter_with&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
    def project: &lt;span class="o"&gt;(&lt;/span&gt;untyped params&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
    def applicable?: &lt;span class="o"&gt;(&lt;/span&gt;untyped params&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; &lt;span class="o"&gt;(&lt;/span&gt;::TrueClass | untyped&lt;span class="o"&gt;)&lt;/span&gt;
    def to_method_name: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; untyped

    private

    def build_method_name: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; ::String
    def fetch_value: &lt;span class="o"&gt;(&lt;/span&gt;untyped params, untyped field&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
    def empty?: &lt;span class="o"&gt;(&lt;/span&gt;untyped val&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; &lt;span class="o"&gt;(&lt;/span&gt;::FalseClass | untyped&lt;span class="o"&gt;)&lt;/span&gt;
  end
end

&lt;span class="c"&gt;# &amp;lt;truncated&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first option (&lt;code&gt;prototype rb&lt;/code&gt;) generates a signature for all the entities specified in the file (or files) you pass using static analysis (more precisely, via parsing the source code and analyzing ASTs).&lt;/p&gt;

&lt;p&gt;This command &lt;em&gt;streams&lt;/em&gt; to the standard output all the found typings. To save the output, one can use redirection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rbs prototype rb lib/&lt;span class="k"&gt;**&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.rb &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sig/rubanok.rbs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'd prefer to mirror signature files to source files (i.e., have multiple files). We can achieve this with some knowledge of Unix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find lib &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="se"&gt;\*&lt;/span&gt;.rb &lt;span class="nt"&gt;-print&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-sd&lt;/span&gt; / &lt;span class="nt"&gt;-f&lt;/span&gt; 2- | xargs &lt;span class="nt"&gt;-I&lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt; bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'export file={}; export target=sig/$file; mkdir -p ${target%/*}; rbs prototype rb lib/$file &amp;gt; sig/${file/rb/rbs}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my opinion, it would be much better if we had the above functionality by default (or maybe that's a feature—keeping all the signatures in the same file 🤔). &lt;/p&gt;

&lt;p&gt;Also, copying comments from source files to signatures makes the latter less readable (especially if there are many comments, like in my case). Of course, we can add a bit more Unix magic to fix this...&lt;/p&gt;

&lt;p&gt;Let's try runtime mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ RUBYOPT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-Ilib"&lt;/span&gt; rbs prototype runtime &lt;span class="nt"&gt;-r&lt;/span&gt; rubanok Rubanok::Rule

class Rubanok::Rule
  public

  def activate_always: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; untyped
  def activate_on: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; untyped
  def applicable?: &lt;span class="o"&gt;(&lt;/span&gt;untyped params&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
  def fields: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; untyped
  def filter_with: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; untyped
  def ignore_empty_values: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; untyped
  def project: &lt;span class="o"&gt;(&lt;/span&gt;untyped params&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
  def to_method_name: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; untyped

  private

  def build_method_name: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; untyped
  def empty?: &lt;span class="o"&gt;(&lt;/span&gt;untyped val&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
  def fetch_value: &lt;span class="o"&gt;(&lt;/span&gt;untyped params, untyped field&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
  def initialize: &lt;span class="o"&gt;(&lt;/span&gt;untyped fields, ?activate_on: untyped, ?activate_always: untyped, ?ignore_empty_values: untyped, ?filter_with: untyped&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this mode, RBS uses Ruby introspection APIs (&lt;code&gt;Class.methods&lt;/code&gt;, etc.) to generate the specified class or module signature.&lt;/p&gt;

&lt;p&gt;Let's compare signatures for the &lt;code&gt;Rubanok::Rule&lt;/code&gt; class generated with &lt;code&gt;rb&lt;/code&gt; and &lt;code&gt;runtime&lt;/code&gt; modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First, runtime generator does not recognize &lt;code&gt;attr_reader&lt;/code&gt; (for instance, &lt;code&gt;activate_on&lt;/code&gt; and &lt;code&gt;activate_always&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Second, runtime generator sorts methods alphabetically while static generator preserves the original layout.&lt;/li&gt;
&lt;li&gt;Finally, the first signature has a few types defined, while the latter has everything &lt;code&gt;untyped&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, why one may find runtime generator useful? I guess there are only one reason for that: &lt;strong&gt;dynamically generated methods&lt;/strong&gt;. Like, for example, in &lt;a href="https://github.com/rails/rails/blob/5384bbc4ce937b4d539518b4e3cefb91e03d4f80/activerecord/lib/active_record/relation/query_methods.rb#L111-L132"&gt;Active Record&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thus, both modes have their advantages and disadvantages and using them both would provide a better &lt;em&gt;signature coverage&lt;/em&gt;. Unfortunately, there is no good way to diff/merge RBS files yet; you have to that manually. Another manual work is to replace &lt;code&gt;untyped&lt;/code&gt; with the actual typing information.&lt;/p&gt;

&lt;p&gt;But wait to make your hands dirty. There is one more player in this game–&lt;a href="https://github.com/ruby/typeprof"&gt;Type Profiler&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Type Profiler infers a program type signatures dynamically during the execution. It &lt;em&gt;spies&lt;/em&gt; all the loaded classes and methods and collects the information about which types have been used as inputs and outputs, analyzes this data, and produces RBS definitions. Under the hood, it uses a custom Ruby interpreter (so, the code is not actually executed). You can find more in the &lt;a href="https://github.com/ruby/typeprof/blob/master/doc/doc.md"&gt;official docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The main difference between TypeProf and RBS is that we need to create a sample script to be used as a profiling entry-point.&lt;/p&gt;

&lt;p&gt;Let's write one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# sig/rubanok_type_profile.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"rubanok"&lt;/span&gt;

&lt;span class="n"&gt;processor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Rubanok&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Processor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="ss"&gt;:q&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="ss"&gt;:|&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="ss"&gt;:sort_by&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:sort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;activate_on: :sort_by&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;having&lt;/span&gt; &lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"asc"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;raw&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;sort_by&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="ss"&gt;sort: &lt;/span&gt;&lt;span class="s2"&gt;"asc"&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;raw&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;project&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;q: &lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;sort_by: &lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;q: &lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;sort_by: &lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's run &lt;code&gt;typeprof&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;typeprof &lt;span class="nt"&gt;-Ilib&lt;/span&gt; sig/rubanok_type_profile.rb &lt;span class="nt"&gt;--exclude-dir&lt;/span&gt; lib/rubanok/rails &lt;span class="nt"&gt;--exclude-dir&lt;/span&gt; lib/rubanok/rspec.rb

&lt;span class="c"&gt;# Classes&lt;/span&gt;
module Rubanok
  VERSION : String

  class Rule
    UNDEFINED : Object
    @method_name : String
    attr_reader fields : untyped
    attr_reader activate_on : Array[untyped]
    attr_reader activate_always : &lt;span class="nb"&gt;false
    &lt;/span&gt;attr_reader ignore_empty_values : untyped
    attr_reader filter_with : nil
    def initialize : &lt;span class="o"&gt;(&lt;/span&gt;untyped, ?activate_on: untyped, ?activate_always: &lt;span class="nb"&gt;false&lt;/span&gt;, ?ignore_empty_values: untyped, ?filter_with: nil&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; nil
    def project : &lt;span class="o"&gt;(&lt;/span&gt;untyped&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
    def applicable? : &lt;span class="o"&gt;(&lt;/span&gt;untyped&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; bool
    def to_method_name : -&amp;gt; String
    private
    def build_method_name : -&amp;gt; String
    def fetch_value : &lt;span class="o"&gt;(&lt;/span&gt;untyped, untyped&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; Object?
    def empty? : &lt;span class="o"&gt;(&lt;/span&gt;nil&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; &lt;span class="nb"&gt;false
  &lt;/span&gt;end

  &lt;span class="c"&gt;# ...&lt;/span&gt;
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nice, now we have some types defined (though most of them are still untyped), we can see methods visibility and even instance variables (something we haven't seen before). The order of methods stayed the same as in the original file—that's good!&lt;/p&gt;

&lt;p&gt;Unfortunately, despite being a runtime analyzer, TypeProf has not so good metaprogramming support. For example, the methods defined using iteration won't be recognized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# a.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;A&lt;/span&gt;
  &lt;span class="sx"&gt;%w[a b]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_index&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;define_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="no"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="no"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;typeprof a.rb

&lt;span class="c"&gt;# Classes&lt;/span&gt;
class A
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(We can handle this with &lt;code&gt;rbs prototype runtime&lt;/code&gt; 😉)&lt;/p&gt;

&lt;p&gt;So, even if you have an executable that provides 100% coverage of your APIs but uses metaprogramming, using just TypeProf is not enough to build a complete types scaffold for your program.&lt;/p&gt;

&lt;p&gt;To sum up, all three different ways to generate initial signatures have their pros and cons, but combining their results could give a very good starting point in adding types to existing code. Hopefully, we'll be able to automate this in the future.&lt;/p&gt;

&lt;p&gt;In Rubanok's case, I did the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generating initial signatures using &lt;code&gt;rbs prototype rb&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Ran &lt;code&gt;typeprof&lt;/code&gt; and used its output to add missing instance variables and update some signatures.&lt;/li&gt;
&lt;li&gt;Finally, ran &lt;code&gt;rbs prototype runtime&lt;/code&gt; for main classes.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;While I was writing this article, a PR with &lt;code&gt;attr_reader self.foo&lt;/code&gt; support &lt;a href="https://github.com/ruby/rbs/pull/505"&gt;has been merged&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The latter one helped to find a &lt;a href="https://github.com/ruby/rbs/issues/510"&gt;bug&lt;/a&gt; in the signature generated at the first step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; module Rubanok
&lt;span class="gd"&gt;-  attr_accessor ignore_empty_values: untyped
-  attr_accessor fail_when_no_matches: untyped
&lt;/span&gt;&lt;span class="gi"&gt;+  def self.fail_when_no_matches: () -&amp;gt; untyped
+  def self.fail_when_no_matches=: (untyped) -&amp;gt; untyped
+  def self.ignore_empty_values: () -&amp;gt; untyped
+  def self.ignore_empty_values=: (untyped) -&amp;gt; untyped
&lt;/span&gt; end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Introducing Steep
&lt;/h2&gt;

&lt;p&gt;So far, we've only discussed how to write and generate type signatures. That would be useless if we don't add a type checker to our dev stack.&lt;/p&gt;

&lt;p&gt;As of today, the only type checker supporting RBS is &lt;a href="https://github.com/soutaro/steep"&gt;Steep&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;steep init&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Let's add the &lt;code&gt;steep&lt;/code&gt; gem to our dependencies and generate a configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;steep init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That would generate a default &lt;code&gt;Steepfile&lt;/code&gt; with some configuration. For Rubanok, I updated it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Steepfile&lt;/span&gt;
&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="ss"&gt;:lib&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# Load signatures from sig/ folder&lt;/span&gt;
  &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="s2"&gt;"sig"&lt;/span&gt;
  &lt;span class="c1"&gt;# Check only files from lib/ folder&lt;/span&gt;
  &lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="s2"&gt;"lib"&lt;/span&gt;

  &lt;span class="c1"&gt;# We don't want to type check Rails/RSpec related code&lt;/span&gt;
  &lt;span class="c1"&gt;# (because we don't have RBS files for it)&lt;/span&gt;
  &lt;span class="n"&gt;ignore&lt;/span&gt; &lt;span class="s2"&gt;"lib/rubanok/rails/*.rb"&lt;/span&gt;
  &lt;span class="n"&gt;ignore&lt;/span&gt; &lt;span class="s2"&gt;"lib/rubanok/railtie.rb"&lt;/span&gt;
  &lt;span class="n"&gt;ignore&lt;/span&gt; &lt;span class="s2"&gt;"lib/rubanok/rspec.rb"&lt;/span&gt;

  &lt;span class="c1"&gt;# We use Set standard library; its signatures&lt;/span&gt;
  &lt;span class="c1"&gt;# come with RBS, but we need to load them explicitly&lt;/span&gt;
  &lt;span class="n"&gt;library&lt;/span&gt; &lt;span class="s2"&gt;"set"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;steep stats&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Before drowning in a sea of types, let's think of how we can measure our signatures' efficiency. We can use &lt;code&gt;steep stats&lt;/code&gt; to see how good (or bad?) our types coverage is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;steep stats &lt;span class="nt"&gt;--log-level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fatal

Target,File,Status,Typed calls,Untyped calls,All calls,Typed %
lib,lib/rubanok/dsl/mapping.rb,success,7,2,11,63.64
lib,lib/rubanok/dsl/matching.rb,success,26,18,50,52.00
lib,lib/rubanok/processor.rb,success,34,8,49,69.39
lib,lib/rubanok/rule.rb,success,24,12,36,66.67
lib,lib/rubanok/version.rb,success,0,0,0,0
lib,lib/rubanok.rb,success,8,4,12,66.67
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command outputs surprisingly outputs CSV 😯. Let's add some Unix magic and make the output more readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;steep stats &lt;span class="nt"&gt;--log-level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fatal | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt; &lt;span class="s1"&gt;'{ printf "%-28s %-9s %-12s %-14s %-10s\n", $2, $3, $4, $5, $7 }'&lt;/span&gt;
File                         Status    Typed calls  Untyped calls  Typed %   
lib/rubanok/dsl/mapping.rb   success   7            2              63.64     
lib/rubanok/dsl/matching.rb  success   26           18             52.00     
lib/rubanok/processor.rb     success   34           8              69.39     
lib/rubanok/rule.rb          success   24           12             66.67     
lib/rubanok/version.rb       success   0            0              0         
lib/rubanok.rb               success   8            4              66.67  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ideally, we would like to have everything typed. So, I opened my &lt;code&gt;.rbs&lt;/code&gt; files and started replacing &lt;code&gt;untyped&lt;/code&gt; with the actual types one by one.&lt;/p&gt;

&lt;p&gt;It took me about a dozen minutes to get rid of untyped definitions (most of them). I'm not going to describe this process in detail; it was pretty straightforward except for the one thing I'd like to pay attention to.&lt;/p&gt;

&lt;p&gt;Let's recall what Rubanok is. It provides a DSL to define &lt;em&gt;data&lt;/em&gt; (usually, user input) transformers of a form &lt;code&gt;(input, params) -&amp;gt; input&lt;/code&gt;. A typical use case is to customize an Active Record relation depending on request parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PagySearchyProcess&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Rubanok&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Processor&lt;/span&gt;
  &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="ss"&gt;:page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:per_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;activate_always: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="ss"&gt;page: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;per_page: &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
   &lt;span class="c1"&gt;# raw is a user input&lt;/span&gt;
   &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;per&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per_page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="ss"&gt;:q&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="ss"&gt;:|&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;PagySearchyProcessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;q: &lt;/span&gt;&lt;span class="s2"&gt;"rbs"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;#=&amp;gt; Post.search("rbs").page(1).per(20)&lt;/span&gt;

&lt;span class="no"&gt;PagySearchyProcessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;q: &lt;/span&gt;&lt;span class="s2"&gt;"rbs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;page: &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;#=&amp;gt; Post.search("rbs").page(2).per(20)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thus, Rubanok deals with two external types: &lt;em&gt;input&lt;/em&gt; (which could be anything) and &lt;em&gt;params&lt;/em&gt; (which is a Hash with String or Symbol keys). Also, we have a notion of &lt;em&gt;field&lt;/em&gt; internally: a params key used to activate a particular transformation. A lot of Rubanok's methods use these three entities, and to avoid duplication, I decided to use the &lt;em&gt;type aliases&lt;/em&gt; feature of RBS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Rubanok&lt;/span&gt;
  &lt;span class="c1"&gt;# Transformation parameters&lt;/span&gt;
  &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;Symbol&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;untyped&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Symbol&lt;/span&gt;
  &lt;span class="c1"&gt;# Transformation target (we assume that input and output types are the same)&lt;/span&gt;
  &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Processor&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;
                 &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fields_set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Rule&lt;/span&gt;
    &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;fields: &lt;/span&gt;&lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;applicable?&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That allowed me to avoid duplication and indicate that they are not just Hashes, Strings, or whatever passing around, but &lt;em&gt;params&lt;/em&gt;, &lt;em&gt;fields&lt;/em&gt; and &lt;em&gt;inputs&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Now, let's check our signatures!&lt;/p&gt;

&lt;h2&gt;
  
  
  Fighting with signatures, or make &lt;code&gt;steep check&lt;/code&gt; happy
&lt;/h2&gt;

&lt;p&gt;It's very unlikely that we wrote 100% correct signatures right away. I got ~30 errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;steep check &lt;span class="nt"&gt;--log-level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fatal

lib/rubanok/dsl/mapping.rb:24:8: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;map &lt;span class="o"&gt;(&lt;/span&gt;def map&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;fields, &lt;span class="k"&gt;**&lt;/span&gt;options, &amp;amp;block&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/dsl/mapping.rb:38:10: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Object &amp;amp; ::Rubanok::DSL::Mapping::ClassMethods&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;define_method &lt;span class="o"&gt;(&lt;/span&gt;define_method&lt;span class="o"&gt;(&lt;/span&gt;rule.to_method_name, &amp;amp;block&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/dsl/mapping.rb:40:10: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Object &amp;amp; ::Rubanok::DSL::Mapping::ClassMethods&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;add_rule &lt;span class="o"&gt;(&lt;/span&gt;add_rule rule&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/dsl/matching.rb:25:10: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;initialize &lt;span class="o"&gt;(&lt;/span&gt;def initialize&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;, fields, values &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;, &lt;span class="k"&gt;**&lt;/span&gt;options, &amp;amp;block&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/dsl/matching.rb:26:26: UnexpectedSplat: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;**&lt;/span&gt;options&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/dsl/matching.rb:29:12: IncompatibleAssignment:  ... 
lib/rubanok/dsl/matching.rb:30:32: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Array[untyped], &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;keys &lt;span class="o"&gt;(&lt;/span&gt;@values.keys&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/dsl/matching.rb:42:8: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;initialize &lt;span class="o"&gt;(&lt;/span&gt;def initialize&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;, &lt;span class="k"&gt;**&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/dsl/matching.rb:70:8: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;match &lt;span class="o"&gt;(&lt;/span&gt;def match&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;fields, &lt;span class="k"&gt;**&lt;/span&gt;options, &amp;amp;block&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/dsl/matching.rb:71:17: IncompatibleArguments: ...
lib/rubanok/dsl/matching.rb:73:10: BlockTypeMismatch: ...
lib/rubanok/dsl/matching.rb:75:10: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Object &amp;amp; ::Rubanok::DSL::Matching::ClassMethods&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;define_method &lt;span class="o"&gt;(&lt;/span&gt;define_method&lt;span class="o"&gt;(&lt;/span&gt;rule.to_method_name&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; |params &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;|&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/dsl/matching.rb:83:12: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Object &amp;amp; ::Rubanok::DSL::Matching::ClassMethods&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;define_method &lt;span class="o"&gt;(&lt;/span&gt;define_method&lt;span class="o"&gt;(&lt;/span&gt;clause.to_method_name, &amp;amp;clause.block&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/dsl/matching.rb:86:10: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Object &amp;amp; ::Rubanok::DSL::Matching::ClassMethods&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;add_rule &lt;span class="o"&gt;(&lt;/span&gt;add_rule rule&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/dsl/matching.rb:96:15: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Object &amp;amp; ::Rubanok::DSL::Matching&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;raw &lt;span class="o"&gt;(&lt;/span&gt;raw&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/processor.rb:36:6: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;call &lt;span class="o"&gt;(&lt;/span&gt;def call&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;args&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/processor.rb:56:13: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Class | nil&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;superclass &amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; Processor&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/processor.rb:57:12: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Class | nil&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rules &lt;span class="o"&gt;(&lt;/span&gt;superclass.rules&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/processor.rb:67:13: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Class | nil&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;superclass &amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; Processor&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/processor.rb:68:12: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Class | nil&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fields_set &lt;span class="o"&gt;(&lt;/span&gt;superclass.fields_set&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/processor.rb:78:21: ArgumentTypeMismatch: &lt;span class="nv"&gt;receiver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Hash[::Symbol, untyped], &lt;span class="nv"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Array[::Symbol], &lt;span class="nv"&gt;actual&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Set[::Rubanok::field] &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;fields_set&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/processor.rb:116:6: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Rubanok::Processor, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;self.input &lt;span class="o"&gt;=)&lt;/span&gt;
lib/rubanok/processor.rb:134:6: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Rubanok::Processor, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;self.input &lt;span class="o"&gt;=&lt;/span&gt; prepared_input&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/rule.rb:11:6: IncompatibleAssignment: ...
lib/rubanok/rule.rb:20:8: UnexpectedJumpValue &lt;span class="o"&gt;(&lt;/span&gt;next acc&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/rule.rb:48:12: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Method | nil&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;call &lt;span class="o"&gt;(&lt;/span&gt;filter_with.call&lt;span class="o"&gt;(&lt;/span&gt;val&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/rule.rb:57:8: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;empty? &lt;span class="o"&gt;(&lt;/span&gt;def empty?&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/rule.rb:63:8: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;empty? &lt;span class="o"&gt;(&lt;/span&gt;def empty?&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/rule.rb:69:4: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;empty? &lt;span class="o"&gt;(&lt;/span&gt;def empty?&lt;span class="o"&gt;(&lt;/span&gt;val&lt;span class="o"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's take a closer look at these errors and try to fix them.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Refinements always break things.
&lt;/h3&gt;

&lt;p&gt;Let's start with the last three reported errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lib/rubanok/rule.rb:57:8: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;empty? &lt;span class="o"&gt;(&lt;/span&gt;def empty?&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/rule.rb:63:8: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;empty? &lt;span class="o"&gt;(&lt;/span&gt;def empty?&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/rule.rb:69:4: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;empty? &lt;span class="o"&gt;(&lt;/span&gt;def empty?&lt;span class="o"&gt;(&lt;/span&gt;val&lt;span class="o"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why Steep detected three &lt;code&gt;#empty?&lt;/code&gt; methods in the Rule class? It turned out that it considers an &lt;em&gt;anonymous&lt;/em&gt; refinement body to be a part of the class body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;using&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;refine&lt;/span&gt; &lt;span class="no"&gt;NilClass&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;empty?&lt;/span&gt;
      &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;refine&lt;/span&gt; &lt;span class="no"&gt;Object&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;empty?&lt;/span&gt;
      &lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;empty?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;ignore_empty_values&lt;/span&gt;

  &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I submitted an &lt;a href="https://github.com/soutaro/steep/issues/265"&gt;issue&lt;/a&gt; and moved refinements to the top of the file to fix the errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Superclass don't cry 😢
&lt;/h3&gt;

&lt;p&gt;Another interesting issue relates to &lt;code&gt;superclass&lt;/code&gt; usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lib/rubanok/processor.rb:56:13: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Class | nil&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;superclass &amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; Processor&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/processor.rb:57:12: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Class | nil&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rules &lt;span class="o"&gt;(&lt;/span&gt;superclass.rules&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/processor.rb:67:13: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Class | nil&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;superclass &amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; Processor&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corresponding source code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@rules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;superclass&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="no"&gt;Processor&lt;/span&gt;
    &lt;span class="n"&gt;superclass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dup&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a very common pattern to inherit class &lt;em&gt;properties&lt;/em&gt;. Why doesn't it work? First, the &lt;code&gt;superclass&lt;/code&gt; signature says the result is either Class or &lt;code&gt;nil&lt;/code&gt; (though it could be nil only for the BaseObject class, as far as I know). Thus, we cannot use &lt;code&gt;&amp;lt;=&lt;/code&gt; right away (because it's not defined on &lt;code&gt;NilClass&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Even if we &lt;em&gt;unwrap&lt;/em&gt; &lt;code&gt;superclass&lt;/code&gt;, the problem with &lt;code&gt;.rules&lt;/code&gt; would still be there—Steep's flow sensitivity analysis currently doesn't recognize the &lt;code&gt;&amp;lt;=&lt;/code&gt; operator. So, I decided to hack the system and explicitly define the &lt;code&gt;.superclass&lt;/code&gt; signature for the Processor class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# processor.rbs&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Processor&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;superclass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Processor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, my code stays the same; only the types suffer 😈.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Explicit over implicit: handling splats.
&lt;/h3&gt;

&lt;p&gt;So far, we've seen pretty much the same problems as I had with &lt;a href="https://evilmartians.com/chronicles/sorbetting-a-gem"&gt;Sorbet&lt;/a&gt;. Let's take a look at something new.&lt;/p&gt;

&lt;p&gt;Consider this code snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;project&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform_keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:to_sym&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# params is a Hash, fields_set is a Set&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;fields_set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It produces the following type error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lib/rubanok/processor.rb:78:21: ArgumentTypeMismatch: &lt;span class="nv"&gt;receiver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Hash[::Symbol, untyped], &lt;span class="nv"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Array[::Symbol], &lt;span class="nv"&gt;actual&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Set[::Rubanok::field]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Hash#slice&lt;/code&gt; method expects an Array, but we pass a Set. However, we also use a splat (&lt;code&gt;*&lt;/code&gt;) operator, which implicitly tries to convert an object to an array—seems legit, right? Unfortunately, Steep is not so smart yet: we have to add an explicit &lt;code&gt;#to_a&lt;/code&gt; call.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Explicit over implicit, pt. 2: forwarding arguments.
&lt;/h3&gt;

&lt;p&gt;I used the following pattern in a few places:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A DSL method accepts some options as keyword arguments and then pass them to the Rule class initializer. The possible options are strictly defined and enforced in the &lt;code&gt;Rule#initialize,&lt;/code&gt; but we would like to avoid declaring them explicitly just to forward down. Unfortunately, that's only possible if we declare &lt;code&gt;**options&lt;/code&gt; as &lt;code&gt;untyped&lt;/code&gt;—that would make signatures kinda useless.&lt;/p&gt;

&lt;p&gt;So, we have to become more explicit once again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-        def map(*fields, **options, &amp;amp;block)
-          filter = options[:filter_with]
-          rule = Rule.new(fields, **options)
&lt;/span&gt;
+        def map(*fields, activate_on: fields, activate_always: false, ignore_empty_values: Rubanok.ignore_empty_values, filter_with: nil, &amp;amp;block)
&lt;span class="gi"&gt;+          filter = filter_with
+          rule = Rule.new(fields, activate_on: activate_on, activate_always: activate_always, ignore_empty_values: ignore_empty_values, filter_with: filter_with)
&lt;/span&gt;
# and more...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I guess it's time to add &lt;a href="https://github.com/ruby-next/ruby-next#proposed-and-edge-features"&gt;Ruby Next&lt;/a&gt; and use shorthand Hash notation 🙂&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Variadic arguments: annotations to the rescue!
&lt;/h3&gt;

&lt;p&gt;In the recent Rubanok release, I added an ability to skip input for transformations and only use params as the only &lt;code&gt;#call&lt;/code&gt; method argument. That led to the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As in the previous case, we needed to make our signature more explicit and specify the actual arguments instead of the &lt;code&gt;*args&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This is our signature&lt;/span&gt;
&lt;span class="c1"&gt;# (Note that we can define multiple signatures for a method)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;
             &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;

&lt;span class="c1"&gt;# And this is our code (first attempt)&lt;/span&gt;
&lt;span class="no"&gt;UNDEFINED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UNDEFINED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;UNDEFINED&lt;/span&gt;

  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Params could not be nil"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;

  &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This refactoring doesn't pass the type check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;steep lib/rubanok/processor.rb

lib/rubanok/processor.rb:43:24: ArgumentTypeMismatch: &lt;span class="nv"&gt;receiver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Rubanok::Processor, &lt;span class="nv"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Rubanok::params, &lt;span class="nv"&gt;actual&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Rubanok::input | ::Rubanok::params | ::Object&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;params&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, according to Steep, &lt;code&gt;param&lt;/code&gt; could be pretty match anything :( We need to help Steep to make the right decision. I couldn't find a way to do that via RBS, so my last resort was to use &lt;a href="https://github.com/soutaro/steep/blob/master/manual/annotations.md"&gt;annotations&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Yes, even though RBS itself is designed not to pollute your source code, Steep allows you to do that. And in some cases, that's the necessary evil.&lt;/p&gt;

&lt;p&gt;I came up with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UNDEFINED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;UNDEFINED&lt;/span&gt;

  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Params could not be nil"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;

  &lt;span class="c1"&gt;# @type var params: untyped&lt;/span&gt;
  &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We declare &lt;code&gt;params&lt;/code&gt; as &lt;code&gt;untyped&lt;/code&gt; to silence the error. The &lt;code&gt;#call&lt;/code&gt; method signature guarantees that the &lt;code&gt;params&lt;/code&gt; variable satisfies the &lt;code&gt;params&lt;/code&gt; type requirements, so we should be safe here.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Deal with metaprogramming: interfaces.
&lt;/h3&gt;

&lt;p&gt;Since Rubanok provides a DSL, it heavily uses metaprogramming.&lt;br&gt;
For example, we use &lt;code&gt;#define_method&lt;/code&gt; to dynamically generate transformation methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;activate_on: &lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;activate_always: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ignore_empty_values: &lt;/span&gt;&lt;span class="no"&gt;Rubanok&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ignore_empty_values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;filter_with: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;activate_on: &lt;/span&gt;&lt;span class="n"&gt;activate_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;activate_always: &lt;/span&gt;&lt;span class="n"&gt;activate_always&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ignore_empty_values: &lt;/span&gt;&lt;span class="n"&gt;ignore_empty_values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;filter_with: &lt;/span&gt;&lt;span class="n"&gt;filter_with&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;define_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_method_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;add_rule&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's the error we see when running &lt;code&gt;steep check&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lib/rubanok/dsl/mapping.rb:38:10: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Object &amp;amp; ::Rubanok::DSL::Mapping::ClassMethods&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;define_method &lt;span class="o"&gt;(&lt;/span&gt;define_method&lt;span class="o"&gt;(&lt;/span&gt;rule.to_method_name, &amp;amp;block&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/dsl/mapping.rb:40:10: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Object &amp;amp; ::Module &amp;amp; ::Rubanok::DSL::Mapping::ClassMethods&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;add_rule &lt;span class="o"&gt;(&lt;/span&gt;add_rule rule&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hmm, looks like our type checker doesn't know that we're calling the &lt;code&gt;.map&lt;/code&gt; method in the context of the Processor class (we call &lt;code&gt;Processor.extend DSL::Mapping&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;RBS has a concept of a &lt;em&gt;self type&lt;/em&gt; for module: a self type adds requirements to the classes/modules, which include/prepend/extend this module. For example, we can state that we only allow using &lt;code&gt;Mapping::ClassMethods&lt;/code&gt; to extend modules (and not objects, for example):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Module here is a self type&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ClassMethods&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Module&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fixes &lt;code&gt;NoMethodError&lt;/code&gt; for &lt;code&gt;#define_method,&lt;/code&gt; but we still have it for &lt;code&gt;#add_rule&lt;/code&gt;—this is a Processor self method. How can we add this restriction using module self types? It's not allowed to use &lt;code&gt;singleton(SomeClass)&lt;/code&gt; as a self type; only classes and &lt;em&gt;interfaces&lt;/em&gt; are allowed. Yes, RBS has interfaces! Let's give them a try!&lt;/p&gt;

&lt;p&gt;We only use the &lt;code&gt;#add_rule&lt;/code&gt; method in the modules, so we can define an interface as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;interface&lt;/span&gt; &lt;span class="n"&gt;_RulesAdding&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_rule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Rule&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Then we can use this interface in the Processor class itself&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Processor&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="n"&gt;_RulesAdding&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# And in our modules&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Mapping&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ClassMethods&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_RulesAdding&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7. Making Steep happy.
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Other problems I faced with Steep which I converted into issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/soutaro/steep/issues/266"&gt;Method visibility false negatives&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/soutaro/steep/issues/261"&gt;Inability to declare block return values in some cases&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;I added a few more changes to the signatures and the source code to finally make a &lt;code&gt;steep check&lt;/code&gt; pass. The journey was a bit longer than I expected, but in the end, I'm pretty happy with the result—I will continue using RBS and Steep.&lt;/p&gt;

&lt;p&gt;Here is the final stats for Rubanok:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;File                         Status    Typed calls  Untyped calls  Typed %   
lib/rubanok/dsl/mapping.rb   success   11           0              100.00    
lib/rubanok/dsl/matching.rb  success   54           2              94.74     
lib/rubanok/processor.rb     success   52           2              96.30     
lib/rubanok/rule.rb          success   31           2              93.94     
lib/rubanok/version.rb       success   0            0              0         
lib/rubanok.rb               success   12           0              100.00  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Runtime type checking with RBS
&lt;/h2&gt;

&lt;p&gt;Although RBS doesn't provide static type checking capabilities, it comes with &lt;a href="https://github.com/ruby/rbs/blob/master/docs/sigs.md#setting-up-the-test"&gt;runtime testing utils&lt;/a&gt;. By loading a specific file (&lt;code&gt;rbs/test/setup&lt;/code&gt;), you can ask RBS to watch the execution and check that method calls inputs and outputs satisfy signatures.&lt;/p&gt;

&lt;p&gt;Under the hood, &lt;a href="https://github.com/ruby/rbs/blob/71b8e534e0adcc78aa76ffbd0326ffe01594d520/lib/rbs/test/setup.rb#L62-L73"&gt;TracePoint API is used&lt;/a&gt; along with the &lt;a href="https://github.com/ruby/rbs/blob/71b8e534e0adcc78aa76ffbd0326ffe01594d520/lib/rbs/test/hook.rb#L50"&gt;alias method chain trick&lt;/a&gt; to hijack observed methods. Thus, it's meant for use in tests, not in production.&lt;/p&gt;

&lt;p&gt;Let's try to run our RSpec tests with runtime checking enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ RBS_TEST_TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Rubanok::*'&lt;/span&gt; &lt;span class="nv"&gt;RUBYOPT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'-rrbs/test/setup'&lt;/span&gt; bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rspec &lt;span class="nt"&gt;--fail-fast&lt;/span&gt;

I, &lt;span class="o"&gt;[&lt;/span&gt;2020-12-07T21:07:57.221200 &lt;span class="c"&gt;#285]  INFO -- : Setting up hooks for ::Rubanok&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2020-12-07T21:07:57.221302 &lt;span class="c"&gt;#285]  INFO -- rbs: Installing runtime type checker in Rubanok...&lt;/span&gt;
...

Failures:

  1&lt;span class="o"&gt;)&lt;/span&gt; Rails controllers integration PostsApiController#planish implicit rubanok with matching
     Failure/Error: prepare! unless prepared?

     RBS::Test::Tester::TypeError:
       TypeError: &lt;span class="o"&gt;[&lt;/span&gt;Rubanok::Processor#prepared?] ReturnTypeError: expected &lt;span class="sb"&gt;`&lt;/span&gt;bool&lt;span class="sb"&gt;`&lt;/span&gt; but returns &lt;span class="sb"&gt;`&lt;/span&gt;nil&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oh, we forgot to initialize the &lt;code&gt;@prepared&lt;/code&gt; instance variable with the boolean value! Nice!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When I tried to use RBS runtime tests for the first time, I encountered &lt;a href="https://github.com/ruby/rbs/issues?q=is%3Aissue+author%3Apalkan+is%3Aclosed"&gt;a few severe problems&lt;/a&gt;. Many thanks to &lt;a href="https://github.com/soutaro"&gt;Soutaro Matsumoto&lt;/a&gt; for fixing all of them faster than I finished working on this article!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I found a couple of more issues by using &lt;code&gt;rbs/test/setup&lt;/code&gt;, including the one I wasn't able to resolve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Failure/Error: super&lt;span class="o"&gt;(&lt;/span&gt;fields, activate_on: activate_on, activate_always: activate_always&lt;span class="o"&gt;)&lt;/span&gt;

RBS::Test::Tester::TypeError:
  TypeError: &lt;span class="o"&gt;[&lt;/span&gt;Rubanok::Rule#initialize] UnexpectedBlockError: unexpected block is given &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;::Array[::Rubanok::field] fields, ?filter_with: ::Method?, ?ignore_empty_values: bool, ?activate_always: bool, ?activate_on: ::Rubanok::field | ::Array[::Rubanok::field]&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; void&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Clause&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Rubanok&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Rule&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# The block is passed to super implicitly,&lt;/span&gt;
    &lt;span class="c1"&gt;# but is not acceptable by Rule#initialize&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tried to use &lt;code&gt;&amp;amp;nil&lt;/code&gt; to disable block propagation, but that broke &lt;code&gt;steep check&lt;/code&gt; 😞. I submitted &lt;a href="https://github.com/soutaro/steep/issues/268"&gt;an issue&lt;/a&gt; and excluded &lt;code&gt;Rule#initialize&lt;/code&gt; from the runtime checking for now using a special comment in the &lt;code&gt;.rbs&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# rule.rbs&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Rule&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rbs&lt;/span&gt;&lt;span class="ss"&gt;:test:skip&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="ss"&gt;activate_on: &lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="ss"&gt;activate_always: &lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="ss"&gt;ignore_empty_values: &lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="ss"&gt;filter_with: &lt;/span&gt;&lt;span class="no"&gt;Method&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bonus: Steep meets Rake
&lt;/h2&gt;

&lt;p&gt;I usually run &lt;code&gt;be rake&lt;/code&gt; pretty often during development to make sure that everything is correct. The default task usually includes RuboCop and tests.&lt;/p&gt;

&lt;p&gt;Let's add Steep to the party:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Rakefile&lt;/span&gt;

&lt;span class="c1"&gt;# other tasks&lt;/span&gt;

&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;:steep&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# Steep doesn't provide Rake integration yet,&lt;/span&gt;
  &lt;span class="c1"&gt;# but can do that ourselves &lt;/span&gt;
  &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"steep"&lt;/span&gt;
  &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"steep/cli"&lt;/span&gt;

  &lt;span class="no"&gt;Steep&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CLI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;argv: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"check"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;stdout: &lt;/span&gt;&lt;span class="vg"&gt;$stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;stderr: &lt;/span&gt;&lt;span class="vg"&gt;$stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;stdin: &lt;/span&gt;&lt;span class="vg"&gt;$stdin&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="ss"&gt;:steep&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# Let's add a user-friendly shortcut&lt;/span&gt;
  &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;:stats&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="sx"&gt;%q(bundle exec steep stats --log-level=fatal | awk -F',' '{ printf "%-28s %-9s %-12s %-14s %-10s\n", $2, $3, $4, $5, $7 }')&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Run steep before everything else to fail-fast&lt;/span&gt;
&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="sx"&gt;%w[steep rubocop rubocop:md spec]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bonus 2: Type Checking meets GitHub Actions
&lt;/h2&gt;

&lt;p&gt;As the final step, I configure GitHub Actions to run both static and runtime type checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lint.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;steep&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@v1&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2.7&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Steep check&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;gem install steep&lt;/span&gt;
        &lt;span class="s"&gt;steep check&lt;/span&gt;

&lt;span class="c1"&gt;# rspec.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rspec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run RSpec with RBS&lt;/span&gt;
      &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;matrix.ruby == '2.7'&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;gem install rbs&lt;/span&gt;
        &lt;span class="s"&gt;RBS_TEST_TARGET="Rubanok::*" RUBYOPT="-rrbs/test/setup" bundle exec rspec --force-color&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run RSpec without RBS&lt;/span&gt;
      &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;matrix.ruby != '2.7'&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;bundle exec rspec --force-color&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Although there are still enough rough edges, I enjoyed using RBS/Steep a bit more than "eating" Sorbet (mostly because I'm not a big fan of type annotations in the source code). I will continue adopting Ruby 3 types in my OSS projects and reporting as many issues to RBS/Steep as possible 🙂.&lt;/p&gt;

&lt;p&gt;P.S. You can find the source code in &lt;a href="https://github.com/palkan/rubanok/pull/15/files"&gt;this PR&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>types</category>
      <category>steep</category>
    </item>
    <item>
      <title>RuboCoping with legacy</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Tue, 24 Mar 2020 21:38:35 +0000</pubDate>
      <link>https://dev.to/evilmartians/rubocoping-with-legacy-gme</link>
      <guid>https://dev.to/evilmartians/rubocoping-with-legacy-gme</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally posted in &lt;a href="https://evilmartians.com/chronicles/rubocoping-with-legacy-bring-your-ruby-code-up-to-standard"&gt;Martian Chronicles&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You will hardly find a Ruby developer who hasn't heard about &lt;a href="https://docs.rubocop.org"&gt;RuboCop&lt;/a&gt;, &lt;em&gt;the&lt;/em&gt; Ruby linter and formatter. And still, it is not that hard to find a project where code style is not enforced. Usually, these are large, mature codebases, often successful ones. Fixing linting and formatting can be a challenge if it wasn't set up correctly from the get-go. So, your RuboCop sees red! Here's how to fix it.&lt;/p&gt;

&lt;p&gt;In this post, I will show you how we at Evil Martians touch up codebases of our customers in 2020: from quick and dirty hacks to proper &lt;a href="https://github.com/testdouble/standard"&gt;Standard&lt;/a&gt;-enforced style guides, and our own &lt;del&gt;patented&lt;/del&gt; way to use Standard and RuboCop configs together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Style matters
&lt;/h2&gt;

&lt;p&gt;Let's pretend I have to convince you to follow code style guidelines (I know, I know I don't have to!)&lt;/p&gt;

&lt;p&gt;Here are the arguments I would use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers understand each other much better when they &lt;del&gt;speak&lt;/del&gt; write the same language.&lt;/li&gt;
&lt;li&gt;Onboarding new engineers becomes much easier when the code style is standardized.&lt;/li&gt;
&lt;li&gt;Linters help to detect and squash bugs in time.&lt;/li&gt;
&lt;li&gt;No more "single vs. double quotes" holy wars (double FTW)!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was all the theory for today. Time for practice!&lt;/p&gt;

&lt;h2&gt;
  
  
  TODO or not TODO
&lt;/h2&gt;

&lt;p&gt;So, you have joined a project with no style guide or with a &lt;code&gt;.rubocop.yml&lt;/code&gt; that was added years ago. You run RuboCop, and you see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rubocop

3306 files inspected, 12418 offenses detected
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Flocks of noble &lt;del&gt;knights&lt;/del&gt; developers tried to &lt;del&gt;slay the beast&lt;/del&gt; fix the offenses but gave up. But that doesn't stop you—you know the magic spell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rubocop &lt;span class="nt"&gt;--auto-gen-config&lt;/span&gt;
Added inheritance from &lt;span class="sb"&gt;`&lt;/span&gt;.rubocop_todo.yml&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;.rubocop.yml&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Created .rubocop_todo.yml.

&lt;span class="nv"&gt;$ &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rubocop
3306 files inspected, no offenses detected
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;That was simple! &lt;em&gt;Toss the coin to your...&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's take a closer look at what &lt;code&gt;--auto-gen-config&lt;/code&gt; flag does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First, it collects all the offenses and their counts;&lt;/li&gt;
&lt;li&gt;then, it generates a &lt;code&gt;.rubocop_todo.yml&lt;/code&gt; where all the current offenses are ignored;&lt;/li&gt;
&lt;li&gt;and finally, it makes &lt;code&gt;.rubocop.yml&lt;/code&gt; inherit from &lt;code&gt;.rubocop_todo.yml&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the way to set the status quo and only enforce style checks for new code. Sounds smart, right? Not exactly.&lt;/p&gt;

&lt;p&gt;The way &lt;code&gt;.rubocop_todo.yml&lt;/code&gt; handles "ignores" depends on the cop types and the total number of current offenses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For metrics cops (such as &lt;code&gt;Layout/LineLength&lt;/code&gt;), the limit (&lt;code&gt;Max&lt;/code&gt;) is set to the maximum value for the current codebase.&lt;/li&gt;
&lt;li&gt;All cops could be disabled if the total number of offenses hits the threshold (only 15 by default).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, you end up with &lt;em&gt;anything goes&lt;/em&gt; situation, and that defeats the purpose.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://medium.com/@scottm/rubocop-in-legacy-projects-part-1-todos-and-todonts-877ace9f23b7"&gt;This article&lt;/a&gt; covers this problem in more detail.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What does it mean for a typical legacy codebase? Most of the new code would be ignored by RuboCop, too. We made the tool happy, but are &lt;em&gt;we&lt;/em&gt; happy with it?&lt;/p&gt;

&lt;p&gt;Hopefully, there is a way to generate a better TODO config by adding more options to the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rubocop –-auto-gen-config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--auto-gen-only-exclude&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--exclude-limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Where &lt;code&gt;--auto-gen-only-exclude&lt;/code&gt; force-excludes metrics cops instead of changing their &lt;code&gt;Max&lt;/code&gt; value, and &lt;code&gt;--exclude-limit&lt;/code&gt; sets the threshold for the exclusion (set to some large enough number to avoid disabling cops completely).&lt;/p&gt;

&lt;p&gt;Now your &lt;code&gt;.rubocop_todo.yml&lt;/code&gt; won't affect your new files or entirely new offenses in the old ones.&lt;/p&gt;

&lt;p&gt;RuboCop doesn't only help with style—it also saves you from common mistakes that can break your code in production. What if you had some bugs and ignored them in your TODO config? What are the cops that should never be ignored? Let me introduce the &lt;em&gt;RuboCop strict configuration&lt;/em&gt; pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  You shall not pass: introducing &lt;code&gt;.rubocop_strict.yml&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;There are a handful of cops that must be enabled for all the files independently of the &lt;code&gt;.rubocop_todo.yml&lt;/code&gt;. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Lint/Debugger&lt;/code&gt;—don't leave debugging calls (e.g., &lt;code&gt;binding.pry&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RSpec/Focus&lt;/code&gt; (from &lt;a href="https://github.com/rubocop-hq/rubocop-rspec"&gt;&lt;code&gt;rubocop-rspec&lt;/code&gt;&lt;/a&gt;)—don't forget to clear focused tests (to make sure CI runs the whole test suite).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We put such cops into a &lt;a href="https://gist.github.com/palkan/ee1d0247be2076e38020a9a6fbae68d5"&gt;&lt;code&gt;.rubocop_strict.yml&lt;/code&gt;&lt;/a&gt; configuration file like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;inherit_from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_todo.yml&lt;/span&gt;

&lt;span class="s"&gt;Lint/Debugger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# don't leave binding.pry&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;Exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;

&lt;span class="s"&gt;RSpec/Focus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# run ALL tests on CI&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;Exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;

&lt;span class="s"&gt;Rails/Output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Don't leave puts-debugging&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;Exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;

&lt;span class="s"&gt;Rails/FindEach&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# each could severely affect the performance, use find_each&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;Exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;

&lt;span class="s"&gt;Rails/UniqBeforePluck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# uniq.pluck and not pluck.uniq&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;Exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Then, we replace the TODO config with the Strict config in our base &lt;code&gt;.rubocop.yml&lt;/code&gt; configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight diff"&gt;&lt;code&gt; inherit_from:
&lt;span class="gd"&gt;-  - .rubocop_todo.yml
&lt;/span&gt;&lt;span class="gi"&gt;+  - .rubocop_strict.yml
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Exclude: []&lt;/code&gt; is crucial here: even if our &lt;code&gt;.rubocop_todo.yml&lt;/code&gt; contained exclusions for &lt;em&gt;strict&lt;/em&gt; cops, we &lt;em&gt;nullify&lt;/em&gt; them here, thus, re-activating these cops for all the files.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Standard to rule them all
&lt;/h2&gt;

&lt;p&gt;One of the biggest problems in adopting a code style is to convince everyone on the team to always use double-quotes for strings, or to add trailing commas to multiline arrays, or ro &amp;lt;choose-your-own-controversal-style-rule&amp;gt;? We are all well familiar with bikeshedding.&lt;/p&gt;

&lt;p&gt;RuboCop provides a default configuration based on the &lt;a href="https://github.com/rubocop-hq/ruby-style-guide"&gt;Ruby Style Guide&lt;/a&gt;. And you know what? It's hard to find a project which follows all of the default rules, there are always reconfigured or disabled cops in the &lt;code&gt;.rubocop.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's okay. RuboCop's default configuration is not a &lt;em&gt;golden standard&lt;/em&gt;; it was never meant to be the one style to fit them all.&lt;/p&gt;

&lt;p&gt;Should Ruby community have &lt;em&gt;the only style&lt;/em&gt; at all? It seems that yes, we need it.&lt;/p&gt;

&lt;p&gt;I think the main reason for that is the popularity of auto-formatters in other programming languages: JavaScript, Go, Rust, Elixir. Auto-formatters are usually very strict and allow almost none or zero configuration. And developers got used to that! People like writing code without worrying about indentation, brackets, and spaces; &lt;em&gt;robots&lt;/em&gt; would sort it all out!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Checkout out the &lt;a href="https://www.youtube.com/watch?v=uLyV5hOqGQ8"&gt;lightning talk&lt;/a&gt; and let Justin Searls convince you to switch to &lt;a href="https://github.com/testdouble/standard"&gt;Standard&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thankfully, Ruby's ecosystem has got you covered: there is a project called &lt;a href="https://github.com/testdouble/standard"&gt;Standard&lt;/a&gt;, which claims to be &lt;em&gt;the one and only Ruby style guide&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;From the technical point of view, Standard is a wrapper over RuboCop with its custom configuration and CLI (&lt;code&gt;standard&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Unfortunately, Standard lacks some RuboCop features that are essential at the early stages of a style guide's adoption: it is not possible to use &lt;code&gt;.rubocop_todo.yml&lt;/code&gt; or any other local configuration. It also &lt;a href="https://github.com/testdouble/standard/issues/135"&gt;doesn't support&lt;/a&gt; RuboCop plugins or custom cops.&lt;/p&gt;

&lt;p&gt;But we can still use Standard as a style guide while continuing to use RuboCop as a linter and formatter!&lt;/p&gt;

&lt;p&gt;For that, we can use RuboCop's &lt;code&gt;inherit_gem&lt;/code&gt; directive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .rubocop.yml&lt;/span&gt;

&lt;span class="c1"&gt;# We want Exclude directives from different&lt;/span&gt;
&lt;span class="c1"&gt;# config files to get merged, not overwritten&lt;/span&gt;
&lt;span class="na"&gt;inherit_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;merge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Exclude&lt;/span&gt;

&lt;span class="na"&gt;require&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Performance cops are bundled with Standard&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rubocop-performance&lt;/span&gt;
  &lt;span class="c1"&gt;# Standard's config uses this custom cop,&lt;/span&gt;
  &lt;span class="c1"&gt;# so it must be loaded&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;standard/cop/semantic_blocks&lt;/span&gt;

&lt;span class="na"&gt;inherit_gem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;standard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config/base.yml&lt;/span&gt;

&lt;span class="na"&gt;inherit_from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_strict.yml&lt;/span&gt;

&lt;span class="c1"&gt;# Sometimes we enable metrics cops&lt;/span&gt;
&lt;span class="c1"&gt;# (which are disabled in Standard by default)&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Metrics:&lt;/span&gt;
&lt;span class="c1"&gt;#   Enabled: true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;That is the configuration I use in most of my OSS and commercial projects. I can't say I agree with all the rules, but I definitely like it more than the RuboCop's default. That is a tiny trade-off if you think about the benefit of not arguing over the style anymore.&lt;/p&gt;

&lt;p&gt;Don't forget to add &lt;code&gt;standard&lt;/code&gt; to your Gemfile and freeze its minor version to avoid unexpected failures during upgrades:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"standard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 0.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;require: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Although the approach above allows you to tinker with the Standard configuration, I would not recommend doing that. Use this flexibility to extend the default behavior, not change it!&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond the Standard
&lt;/h2&gt;

&lt;p&gt;RuboCop has a lot of plugins distributed as separate gems: &lt;a href="https://github.com/rubocop-hq/rubocop-rspec"&gt;&lt;code&gt;rubocop-rspec&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/rubocop-hq/rubocop-performance"&gt;&lt;code&gt;rubocop-performance&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/rubocop-hq/rubocop-rails"&gt;&lt;code&gt;rubocop-rails&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/rubocop-hq/rubocop-md"&gt;&lt;code&gt;rubocop-md&lt;/code&gt;&lt;/a&gt;, to name a few.&lt;/p&gt;

&lt;p&gt;Standard only includes the &lt;code&gt;rubocop-performance&lt;/code&gt; plugin. We usually add &lt;code&gt;rubocop-rails&lt;/code&gt; and &lt;code&gt;rubocop-rspec&lt;/code&gt; to our configuration.&lt;/p&gt;

&lt;p&gt;For each plugin, we keep a separate YAML file: &lt;code&gt;.rubocop_rails.yml&lt;/code&gt;, &lt;code&gt;.rubocop_rspec.yml&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;Inside the base config we add these files to &lt;code&gt;inherit_from&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;inherit_from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_rails.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_rspec.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_strict.yml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Our &lt;a href="https://gist.github.com/palkan/24869b835c45e89116b9727b534e579e"&gt;&lt;code&gt;.rubocop_rails.yml&lt;/code&gt;&lt;/a&gt; is based on the &lt;a href="https://github.com/testdouble/standard/commit/94d133f477a5694084ac974d5ee01e8a66ce777e#diff-65478e10d5b2ef41c7293a110c0e6b7c"&gt;configuration that existed in Standard&lt;/a&gt; before they dropped Rails support.&lt;/p&gt;

&lt;p&gt;There is no standard RSpec configuration, so we had to figure out our own: &lt;a href="https://gist.github.com/palkan/623c0816b05ed246bfe0cb406050990a"&gt;&lt;code&gt;.rubocop_rspec.yml&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We also usually enable a select few &lt;em&gt;custom&lt;/em&gt; cops, for example, &lt;a href="https://github.com/evilmartians/terraforming-rails/tree/master/tools/lint_env"&gt;&lt;code&gt;Lint/Env&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the end, our typical RuboCop configuration for Rails projects looks like this👇&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .rubocop.yml&lt;/span&gt;
&lt;span class="na"&gt;inherit_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;merge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Exclude&lt;/span&gt;

&lt;span class="na"&gt;require&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rubocop-performance&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;standard/cop/semantic_blocks&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./lib/cops/lint/env.rb&lt;/span&gt;

&lt;span class="na"&gt;inherit_gem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;standard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config/base.yml&lt;/span&gt;

&lt;span class="na"&gt;inherit_from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_rails.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_rspec.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_strict.yml&lt;/span&gt;

&lt;span class="s"&gt;Lint/Env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;Include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.rb'&lt;/span&gt;
  &lt;span class="na"&gt;Exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/config/environments/**/*'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/config/application.rb'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/config/environment.rb'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/config/puma.rb'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/config/boot.rb'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/spec/*_helper.rb'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/spec/**/support/**/*'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lib/generators/**/*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Feel free to use it as an inspiration for your projects that could use some RuboCop's tough love.&lt;/p&gt;




&lt;p&gt;RuboCop plays a vital role in the Ruby world and will stay TOP-1 for linting and formatting code for quite a long time (though competing formatters are evolving, for example, &lt;a href="https://github.com/penelopezone/rubyfmt/"&gt;rubyfmt&lt;/a&gt; and &lt;a href="https://github.com/prettier/plugin-ruby"&gt;prettier-ruby&lt;/a&gt;). Don't ignore RuboCop; write code in style 😎&lt;/p&gt;




&lt;p&gt;Read more dev articles on &lt;a href="https://evilmartians.com/chronicles"&gt;https://evilmartians.com/chronicles&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>linters</category>
      <category>formatters</category>
      <category>rubocop</category>
    </item>
    <item>
      <title>Pulling the trigger: How to update counter caches in your Rails app without Active Record callbacks</title>
      <dc:creator>Dmitry Tsepelev</dc:creator>
      <pubDate>Tue, 26 Nov 2019 10:13:09 +0000</pubDate>
      <link>https://dev.to/evilmartians/pulling-the-trigger-how-to-update-counter-caches-in-your-rails-app-without-active-record-callbacks-3l28</link>
      <guid>https://dev.to/evilmartians/pulling-the-trigger-how-to-update-counter-caches-in-your-rails-app-without-active-record-callbacks-3l28</guid>
      <description>&lt;p&gt;In this article, we experiment with &lt;a href="https://en.wikipedia.org/wiki/Database_trigger"&gt;triggers&lt;/a&gt; as a tool for keeping aggregated data consistent when using Active Record and your favorite SQL database. Instead of using sophisticated tools such as ElasticSearch for filtering and searching, we will demonstrate a simple approach that achieves the same result with some out-of-the-box database features. As a bonus, learn how to avoid nasty race conditions!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can find all the examples in the &lt;a href="https://gist.github.com/DmitryTsepelev/82a44170b2317442df2d54a4c848cb6f"&gt;gist&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sometimes you need to sort and filter records in the database by some &lt;a href="https://www.postgresql.org/docs/current/functions-aggregate.html"&gt;aggregated&lt;/a&gt; values. For instance, you might be building a paginated list of users in an admin panel, and you want to implement filtering by the number of orders and the total amount that users have spent on them. There are several tools like &lt;a href="https://www.elastic.co/what-is/elasticsearch"&gt;ElasticSearch&lt;/a&gt;, which are good at filtering by aggregates, but setting up a massive search engine and all required infrastructure to process a couple of columns sounds like an overkill. Let's find a more straightforward way!&lt;/p&gt;

&lt;h2&gt;
  
  
  Trigger finger
&lt;/h2&gt;

&lt;p&gt;Imagine the following data model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="s2"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;force: :cascade&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bigint&lt;/span&gt; &lt;span class="s2"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decimal&lt;/span&gt; &lt;span class="s2"&gt;"amount"&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt; &lt;span class="s2"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;precision: &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt; &lt;span class="s2"&gt;"updated_at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;precision: &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"index_orders_on_user_id"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="s2"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;force: :cascade&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt; &lt;span class="s2"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;precision: &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt; &lt;span class="s2"&gt;"updated_at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;precision: &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;add_foreign_key&lt;/span&gt; &lt;span class="s2"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"users"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:orders&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Let's see how we can filter and paginate users by their order total. We can easily achieve our goal with the vanilla SQL statement, but we will immediately run into performance issues. To demonstrate, let's fill the database with 10,000 users and 100,000 orders and use &lt;a href="https://www.postgresql.org/docs/current/sql-explain.html"&gt;explain&lt;/a&gt; (you can find a single file implementation of this example in &lt;a href="https://gist.github.com/DmitryTsepelev/82a44170b2317442df2d54a4c848cb6f#file-triggers1-rb"&gt;this gist&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;created_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;user_id: &lt;/span&gt;&lt;span class="nb"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;amount: &lt;/span&gt;&lt;span class="nb"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;created_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="sh"&gt;
  EXPLAIN ANALYZE SELECT users.id, SUM(orders.amount), COUNT(orders.id)
  FROM users JOIN orders ON orders.user_id = users.id
  GROUP BY users.id
  HAVING SUM(orders.amount) &amp;gt; 100 AND COUNT(orders.id) &amp;gt; 1
  ORDER BY SUM(orders.amount)
  LIMIT 50
&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This is the result you might see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;Limit&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3206&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;3206&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;29&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;737&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;746&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;Sort&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3206&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;3208&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;95&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1116&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;736&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;739&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;Sort&lt;/span&gt; &lt;span class="k"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;Sort&lt;/span&gt; &lt;span class="k"&gt;Method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="n"&gt;heapsort&lt;/span&gt;  &lt;span class="n"&gt;Memory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="n"&gt;kB&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;HashAggregate&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2968&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;3169&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;09&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1116&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;103&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;452&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="k"&gt;Group&lt;/span&gt; &lt;span class="k"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
              &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'100'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
              &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;Hash&lt;/span&gt; &lt;span class="k"&gt;Join&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;290&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;08&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;2050&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;73&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;73392&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;793&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;022&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;Hash&lt;/span&gt; &lt;span class="n"&gt;Cond&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;Seq&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;1567&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;92&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;73392&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;011&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;650&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;Hash&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;164&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;164&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10048&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;760&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;760&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                          &lt;span class="n"&gt;Buckets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16384&lt;/span&gt;  &lt;span class="n"&gt;Batches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="n"&gt;Memory&lt;/span&gt; &lt;span class="k"&gt;Usage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;519&lt;/span&gt;&lt;span class="n"&gt;kB&lt;/span&gt;
                          &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;Seq&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;164&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10048&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;006&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;220&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;237&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;151&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;With a bigger database, it will take even more time! We need to find a better solution, the one that scales. Let's denormalize our database and store &lt;code&gt;orders_amount&lt;/code&gt; in the separate &lt;code&gt;user_stats&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateUserStats&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;6.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:user_stats&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt; &lt;span class="ss"&gt;:user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decimal&lt;/span&gt; &lt;span class="ss"&gt;:orders_amount&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt; &lt;span class="ss"&gt;:orders_count&lt;/span&gt;

      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt; &lt;span class="ss"&gt;:user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;unique: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Now we should decide how to keep &lt;code&gt;orders_count&lt;/code&gt; and &lt;code&gt;orders_amount&lt;/code&gt; in sync. &lt;a href="https://guides.rubyonrails.org/active_record_callbacks.html"&gt;ActiveRecord callbacks&lt;/a&gt; do not look like a proper place to handle such operations, because we want to have our stats updated even when data is changed with a plain SQL (e.g., in the migration). There is a built-in &lt;code&gt;counter_cache&lt;/code&gt; &lt;a href="https://guides.rubyonrails.org/association_basics.html#options-for-belongs-to-counter-cache"&gt;option&lt;/a&gt; for the &lt;code&gt;belongs_to&lt;/code&gt; association, but it cannot help us with &lt;code&gt;orders_amount&lt;/code&gt;. Triggers to the rescue!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A trigger is a function that is automatically invoked when INSERT, UPDATE, or DELETE is performed on the table.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To work with triggers from our Rails app, we can use gems like &lt;a href="https://github.com/jenseng/hair_trigger"&gt;hair_trigger&lt;/a&gt;, &lt;a href="https://github.com/teoljungberg/fx"&gt;fx&lt;/a&gt;, or even write them by hand. In this example, we use &lt;code&gt;hair_trigger&lt;/code&gt;, which can generate migrations for trigger updates using only the latest version of the SQL procedure.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Heads up! There is a known &lt;a href="https://github.com/jenseng/hair_trigger/issues/84"&gt;hair_trigger issue&lt;/a&gt; with Rails 6 and Zeitwerk, if you face it–feel free to use &lt;a href="https://github.com/DmitryTsepelev/hair_trigger/tree/rails-6"&gt;my fork&lt;/a&gt; for now. Don't forget to switch back when the fix is out!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let's add our trigger to the &lt;code&gt;Order&lt;/code&gt; model. We want to perform the &lt;em&gt;UPSERT&lt;/em&gt;: if there is no row with the matching &lt;code&gt;user_id&lt;/code&gt; in the &lt;code&gt;user_stats&lt;/code&gt; table–we add a new row, otherwise–update the existing one (make sure there is a &lt;code&gt;unique&lt;/code&gt; constraint on the &lt;code&gt;user_id&lt;/code&gt; column):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;

  &lt;span class="n"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:insert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="sh"&gt;
      INSERT INTO user_stats (user_id, orders_amount, orders_count)
      SELECT
        NEW.user_id as user_id,
        SUM(orders.amount) as orders_amount,
        COUNT(orders.id) as orders_count
      FROM orders WHERE orders.user_id = NEW.user_id
      ON CONFLICT (user_id) DO UPDATE
      SET
        orders_amount = EXCLUDED.orders_amount,
        orders_count = EXCLUDED.orders_count;
&lt;/span&gt;&lt;span class="no"&gt;    SQL&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Now we should generate the migration with &lt;code&gt;rake db:generate_trigger_migration&lt;/code&gt;, run migrations with &lt;code&gt;rails db:migrate&lt;/code&gt;, and run the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Off to the races
&lt;/h2&gt;

&lt;p&gt;It might seem to be working, but what if we try to insert multiple orders in parallel? (you can run the following code as a rake task or check my implementation &lt;a href="https://gist.github.com/DmitryTsepelev/82a44170b2317442df2d54a4c848cb6f#file-triggers2-rb"&gt;here&lt;/a&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;

&lt;span class="n"&gt;threads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;threads&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;amount: &lt;/span&gt;&lt;span class="nb"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:join&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;inconsistent_stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UserStat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;joins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;user: :orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                             &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;user_id: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                             &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;having&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"user_stats.orders_amount &amp;lt;&amp;gt; SUM(orders.amount)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                             &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"user_stats.id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;inconsistent_stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any?&lt;/span&gt;
  &lt;span class="n"&gt;calculated_amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UserStat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;orders_amount&lt;/span&gt;
  &lt;span class="n"&gt;real_amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:amount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_f&lt;/span&gt;

  &lt;span class="nb"&gt;puts&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Race condition detected:"&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"calculated amount: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;calculated_amount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"real amount: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;real_amount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Data is consistent."&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;There is a huge chance that there &lt;em&gt;will&lt;/em&gt; be a race condition, but why? The problem is that the trigger runs inside the current transaction, and the default &lt;a href="https://www.postgresql.org/docs/current/transaction-iso.html"&gt;isolation level&lt;/a&gt; is &lt;code&gt;READ COMMITTED&lt;/code&gt;, which cannot handle race conditions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PostgreSQL supports four levels of transaction isolation–READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ and SERIALIZABLE&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The obvious solution is to use a stricter &lt;code&gt;SERIALIZABLE&lt;/code&gt; isolation level, but, unfortunately, an isolation level cannot be changed inside a running transaction. Creating a new explicit transaction every time we work with orders does not sound right either, so let's try another approach for making sure our triggers are always executed in sequence–&lt;a href="https://www.postgresql.org/docs/current/explicit-locking.html"&gt;advisory locks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The only thing we need to change is to add lock &lt;code&gt;PERFORM pg_advisory_xact_lock(NEW.user_id);&lt;/code&gt; at the beginning of our procedure code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;

  &lt;span class="n"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:insert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="sh"&gt;
      PERFORM pg_advisory_xact_lock(NEW.user_id);

      INSERT INTO user_stats (user_id, orders_amount, orders_count)
      SELECT
        NEW.user_id as user_id,
        SUM(orders.amount) as orders_amount,
        COUNT(orders.id) as orders_count
      FROM orders WHERE orders.user_id = NEW.user_id
      ON CONFLICT (user_id) DO UPDATE
      SET
        orders_amount = EXCLUDED.orders_amount,
        orders_count = EXCLUDED.orders_count;
&lt;/span&gt;&lt;span class="no"&gt;    SQL&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;It's way faster! The updated version of the code is &lt;a href="https://gist.github.com/DmitryTsepelev/82a44170b2317442df2d54a4c848cb6f#file-triggers3-rb"&gt;here&lt;/a&gt;, if you run it, you'll see that race condition is gone, and the app can handle parallel requests. Let's add the index to the &lt;code&gt;orders_amount&lt;/code&gt; column in the &lt;code&gt;user_stats&lt;/code&gt; table, change the query, and compare the performance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orders_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orders_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;user_stats&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;orders_amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;orders_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;orders_amount&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;

&lt;span class="k"&gt;Limit&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;059&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;241&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="n"&gt;index_user_stats_on_orders_amount&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;user_stats&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;3438&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;69&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7573&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;058&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Cond&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders_amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'100'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;105&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;272&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  Lock-free alternative
&lt;/h2&gt;

&lt;p&gt;There is a way (suggested by &lt;a href="https://github.com/sponomarev"&gt;Sergey Ponomarev&lt;/a&gt;) to achieve the same result without locks and make it work faster—use &lt;a href="https://www.postgresql.org/docs/current/plpgsql-trigger.html#PLPGSQL-TRIGGER-AUDIT-EXAMPLE"&gt;deltas&lt;/a&gt; (you can find the full implementation &lt;a href="https://gist.github.com/DmitryTsepelev/82a44170b2317442df2d54a4c848cb6f#file-triggers4-rb"&gt;here&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;

  &lt;span class="n"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:insert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="sh"&gt;
      INSERT INTO user_stats (user_id, orders_amount, orders_count)
      SELECT
        NEW.user_id as user_id,
        NEW.amount as orders_amount,
        1 as orders_count
      ON CONFLICT (user_id) DO UPDATE
      SET
        orders_amount = user_stats.orders_amount + EXCLUDED.orders_amount,
        orders_count = user_stats.orders_count + EXCLUDED.orders_count;
&lt;/span&gt;&lt;span class="no"&gt;    SQL&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The trick here is not to use any subqueries, so race conditions would not be possible. As a bonus, you'll get better performance when inserting new records. This approach might come handy for simple cases like the one described in this article, but when you are dealing with more complex logic, you might want to resort to locks (imagine that orders have statuses, we need to cache counts of orders in each status and orders can be updated).&lt;/p&gt;

&lt;h2&gt;
  
  
  Loop instead of UPSERT
&lt;/h2&gt;

&lt;p&gt;In previous examples, we use UPSERT, which was introduced in PostgreSQL 9.5, but what if we use the older version? Let's review how the trigger works again: it tries to insert a new row into the &lt;code&gt;user_stats&lt;/code&gt; table and, if a conflict happens, it updates the existing row. In the real-world application there &lt;em&gt;will&lt;/em&gt; be conflicts most of the time (to be precise–insert happens &lt;em&gt;only once&lt;/em&gt; for each user). We can use this fact and rewrite our trigger in the following way (the example of a loop inside the trigger is &lt;a href="https://gist.github.com/DmitryTsepelev/82a44170b2317442df2d54a4c848cb6f#file-triggers5-rb"&gt;here&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;

  &lt;span class="n"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:insert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="sh"&gt;
      &amp;lt;&amp;lt;insert_update&amp;gt;&amp;gt;
      LOOP
        UPDATE user_stats
        SET orders_count = orders_count + 1,
            orders_amount = orders_amount + NEW.amount
        WHERE user_id = NEW.user_id;

        EXIT insert_update WHEN FOUND;

        BEGIN
          INSERT INTO user_stats (
            user_id, orders_amount, orders_count
          ) VALUES (
            NEW.user_id, 1, NEW.amount
          );

          EXIT insert_update;
        EXCEPTION
          WHEN UNIQUE_VIOLATION THEN
            -- do nothing
        END;
      END LOOP insert_update;
&lt;/span&gt;&lt;span class="no"&gt;    SQL&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In this case, we have inverted our logic: trigger tries to update the existing row, and if it misses—the new row gets inserted.&lt;/p&gt;




&lt;p&gt;Working with aggregated data is hard: when you have a lot of counter (and other kinds of) caches, it makes sense to use a special tool for that. However, for simple cases, we can stay with good old database triggers: when configured properly, they are quite performant!&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Persisted queries in GraphQL: Slim down Apollo requests to your Ruby application</title>
      <dc:creator>Dmitry Tsepelev</dc:creator>
      <pubDate>Thu, 07 Nov 2019 13:24:52 +0000</pubDate>
      <link>https://dev.to/evilmartians/persisted-queries-in-graphql-slim-down-apollo-requests-to-your-ruby-application-43pb</link>
      <guid>https://dev.to/evilmartians/persisted-queries-in-graphql-slim-down-apollo-requests-to-your-ruby-application-43pb</guid>
      <description>&lt;p&gt;Learn how to reduce the size of network requests from the Apollo client in the front-end to the GraphQL Ruby back-end with the help of &lt;a href="https://www.apollographql.com/docs/apollo-server/performance/apq/"&gt;persisted queries&lt;/a&gt;. In this article, we will show how these queries work and set them up both on a client and a server with the help of our &lt;a href="https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries"&gt;graphql-ruby-persisted_queries&lt;/a&gt; Ruby gem.&lt;/p&gt;

&lt;p&gt;One of the benefits of using &lt;a href="https://graphql.org"&gt;GraphQL&lt;/a&gt; is its flexibility: back-end describes the data model using types, so front-end can only get the data it needs. The problem is that the &lt;em&gt;amount&lt;/em&gt; of data required to render a page in a real-world application can be significant, and the query to fetch this data will get out of hand pretty quick. What could help here?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read how Relay deals with persisted queries &lt;a href="https://relay.dev/docs/en/persisted-queries"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;First of all, the amount and the variety of often-used queries in your application are limited: usually, front-end knows precisely what it needs from the back-end for every particular view. Popular GraphQL frameworks like &lt;a href="https://www.apollographql.com"&gt;Apollo&lt;/a&gt; and &lt;a href="https://relay.dev"&gt;Relay&lt;/a&gt; use that fact to &lt;em&gt;persist&lt;/em&gt; queries in the back-end so that a front-end can send just a unique ID over the wire, instead of the full query in all its verboseness. This article will focus on the Apollo implementation on &lt;a href="https://www.apollographql.com/docs/apollo-server/performance/apq/"&gt;persisted queries&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you have the Pro Version of &lt;code&gt;graphql-ruby&lt;/code&gt;–you already have built-in &lt;a href="https://graphql-ruby.org/operation_store/overview.html"&gt;support&lt;/a&gt; for persisted queries.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most of Ruby applications that implement GraphQL server use a gem called &lt;a href="https://graphql-ruby.org"&gt;graphql-ruby&lt;/a&gt;, in this article we are going to find out how to make it work with persisted query IDs coming from the Apollo Client. By default, &lt;code&gt;graphql-ruby&lt;/code&gt; uses POST requests, as query strings tend to become very long, and it makes it hard to configure HTTP caching. If we switch to persisted queries, we will be able to turn on GET requests too and unleash the power of HTTP caching!&lt;/p&gt;

&lt;p&gt;The idea behind persisting queries is pretty straightforward–the back-end should look for a special query parameter, containing the unique ID of the query, find the query in the &lt;em&gt;store&lt;/em&gt; (we'll talk about it later, imagine that it's just a key-value store) and use it to prepare the response.&lt;/p&gt;

&lt;p&gt;Relay has a compiler, and it &lt;em&gt;knows&lt;/em&gt; all the queries in the app, so it can put them to a dedicated text file along with their &lt;em&gt;md5&lt;/em&gt; hashes when &lt;code&gt;--persist-output&lt;/code&gt; option is provided. You can save these queries to your &lt;em&gt;store&lt;/em&gt;, and you're done: back-end is ready to consume IDs instead of full queries.&lt;/p&gt;

&lt;p&gt;Apollo has no compiler, and there is no built-in way to get the list of queries. In this case, queries should be saved to a &lt;em&gt;store&lt;/em&gt; at runtime, and there is a special &lt;a href="https://github.com/apollographql/apollo-link-persisted-queries"&gt;apollo-link-persisted-queries&lt;/a&gt; library that enables this feature. First, change your frontend configuration like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createPersistedQueryLink&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;apollo-link-persisted-queries&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHttpLink&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;apollo-link-http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;InMemoryCache&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;apollo-cache-inmemory&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ApolloClient&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;apollo-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createPersistedQueryLink&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;createHttpLink&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/graphql&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ApolloClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;InMemoryCache&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Now, let's talk about the back-end. With the configured link, client will &lt;em&gt;make an attempt&lt;/em&gt; to send the unique ID (&lt;em&gt;sha256&lt;/em&gt; is used in this case) in the &lt;code&gt;extension&lt;/code&gt; param, e.g.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;persistedQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sha256Hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;688787d8ff144c502c7f5cffaafe2cc588d86079f9de88304c26b0cb99ce91c6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;When server finds the ID in the store—it serves the query if it were sent in full, otherwise–server returns &lt;code&gt;{ errors: [{ message: "PersistedQueryNotFound" }] }&lt;/code&gt; in the response payload. In this case client will send the full query along with unique ID and back-end will persist it to the store.&lt;/p&gt;

&lt;p&gt;Now we need to implement our back-end functionality in the &lt;code&gt;GraphqlController&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GraphqlController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="no"&gt;PersistedQueryNotFound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;StandardError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;
    &lt;span class="n"&gt;query_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;persisted_query&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:query&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;GraphqlSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;query_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;variables: &lt;/span&gt;&lt;span class="n"&gt;ensure_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:variables&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
      &lt;span class="ss"&gt;context: &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;
      &lt;span class="ss"&gt;operation_name: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:operationName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;PersistedQueryNotFound&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;errors: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="s2"&gt;"PersistedQueryNotFound"&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;persisted_query&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:extensions&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;

    &lt;span class="nb"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ensure_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:extensions&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"persistedQuery"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"sha256Hash"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:query&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:query&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;query_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;PersistedQueryNotFound&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;query_str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;

    &lt;span class="n"&gt;query_str&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;store&lt;/span&gt;
    &lt;span class="no"&gt;MemoryStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;instance&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ensure_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ambiguous_param&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Now we need to implement the &lt;code&gt;MemoryStore&lt;/code&gt; (as we know, we need a very simple key-value storage with read and write operations), and that's it, minimalistic back-end support of persisted queries is ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MemoryStore&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;instance&lt;/span&gt;
    &lt;span class="vi"&gt;@instance&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;
    &lt;span class="vi"&gt;@storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@storage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@storage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And that is it! If you configured your client properly and changed your &lt;code&gt;GraphqlController&lt;/code&gt;, it should work! So you don't have to copy all the boilerplate above between projects, I released a little gem called &lt;a href="https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries"&gt;graphql-ruby-persisted_queries&lt;/a&gt;, which implements all the features we discussed earlier, and more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://redis.io"&gt;Redis&lt;/a&gt; storage;&lt;/li&gt;
&lt;li&gt;hash verification;&lt;/li&gt;
&lt;li&gt;hash function configuration;&lt;/li&gt;
&lt;li&gt;Relay support is on its way!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But did not we mention GET requests earlier? Now, we can turn them on, just open up &lt;code&gt;routes.rb&lt;/code&gt; and add the following line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"/graphql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s2"&gt;"graphql#execute"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;We also need to slightly change the initialization of &lt;code&gt;apollo-link-persisted-queries&lt;/code&gt;, which should send queries without query string as GET, and full queries as POST:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createPersistedQueryLink&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;useGETForHashedQueries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;createHttpLink&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/graphql&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;






&lt;p&gt;Persisted queries can help you reduce the amount of traffic between your GraphQL server and its clients by storing query strings at the back-end. If you have the pro version of &lt;code&gt;graphql-ruby&lt;/code&gt;–you're all set, otherwise, take a look at the open-source alternative &lt;a href="https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries"&gt;graphql-ruby-persisted_queries&lt;/a&gt; we have just cooked up.&lt;/p&gt;




&lt;p&gt;Read more dev articles on &lt;a href="https://evilmartians.com/chronicles"&gt;https://evilmartians.com/chronicles&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>graphql</category>
      <category>apollo</category>
    </item>
    <item>
      <title>GitHub Actions: First impressions</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Fri, 06 Sep 2019 10:16:33 +0000</pubDate>
      <link>https://dev.to/evilmartians/github-actions-first-impressions-4ce8</link>
      <guid>https://dev.to/evilmartians/github-actions-first-impressions-4ce8</guid>
      <description>&lt;p&gt;GitHub Actions are coming on strong—in my team, almost everyone who has applied for a beta program, me included, had recently got access to GitHub's latest "killer feature" that threatens to make life harder for Travis CI and CircleCI: Continuous Integration with GitHub &lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;Actions&lt;/a&gt;. Here are my first impressions.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1164301670429409281-5" src="https://platform.twitter.com/embed/Tweet.html?id=1164301670429409281"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1164301670429409281-5');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1164301670429409281&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;For the ultimate test, I decided to reuse the documentation generator setup from my previous article: &lt;a href="https://dev.to/evilmartians/keeping-oss-documentation-with-docsify-lefthook-and-friends-11e5"&gt;"Keeping OSS documentation with Docsify, Lefthook, and friends"&lt;/a&gt;). To lint a documentation website for &lt;a href="https://anycable.io/" rel="noopener noreferrer"&gt;AnyCable&lt;/a&gt;, I used &lt;a href="https://evilmartians.com/chronicles/lefthook-knock-your-teams-code-back-into-shape" rel="noopener noreferrer"&gt;Lefthook&lt;/a&gt; locally and CircleCI for production. For my next documentation project, the one for &lt;a href="https://test-prof.evilmartians.io/" rel="noopener noreferrer"&gt;TestProf&lt;/a&gt;, I decided to move all the CI functionality to GitHub Actions (sorry, Travis and CircleCI, &lt;a href="https://en.wikipedia.org/wiki/Autonomous_spaceport_drone_shiphttps://en.wikipedia.org/wiki/Autonomous_spaceport_drone_ship" rel="noopener noreferrer"&gt;of course I still love you&lt;/a&gt;).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Check the PR with the final implementation: &lt;a href="https://github.com/palkan/test-prof/pull/156" rel="noopener noreferrer"&gt;palkan/test-prof#156&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Warming up: dealing with the stale issues
&lt;/h2&gt;

&lt;p&gt;The first thing I found impressive about GitHub Actions is that they could be used not only to deal with code pushes and pull requests but also react on other &lt;a href="https://help.github.com/en/articles/events-that-trigger-workflows" rel="noopener noreferrer"&gt;GitHub events&lt;/a&gt; or run on schedule!&lt;/p&gt;

&lt;p&gt;One of the &lt;em&gt;actions&lt;/em&gt; that GitHub offers you when you first open the "Actions" tab of your project is &lt;a href="https://github.com/actions/stale" rel="noopener noreferrer"&gt;Stale&lt;/a&gt;: it allows you to mark issues and pull requests with a "stale" label and close them. That's what I usually did by hand (and was hoping to automate with the help of the &lt;a href="https://github.com/apps/stale" rel="noopener noreferrer"&gt;Stale&lt;/a&gt; GitHub App).&lt;/p&gt;

&lt;p&gt;It took me a few minutes to add this action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/stale.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Mark stale issues&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/stale@v1&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;repo-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;stale-issue-message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;⚠ Marking this issue as stale since there has been no activity in the last 30 days.&lt;/span&gt;
          &lt;span class="s"&gt;Remove stale label or comment or this issue will be closed in 15 days ⌛️&lt;/span&gt;
        &lt;span class="na"&gt;stale-issue-label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stale&lt;/span&gt;
        &lt;span class="na"&gt;days-before-stale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
        &lt;span class="na"&gt;days-before-close&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After some time, I received a lot of notifications—the action took action:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fqlr9sbagvo4r9pdf4x1j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fqlr9sbagvo4r9pdf4x1j.png" alt="Stale Comment"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I quickly realized that it wasn't a good idea: GitHub marked all the issues intentionally kept open (for discussion) as stale and spammed all the participants with the comment notification. I'm sorry, guys 😿.&lt;/p&gt;

&lt;p&gt;It turned out that there is no ignore mechanism (e.g., by labels). &lt;a href="https://github.com/actions/stale/pull/11" rel="noopener noreferrer"&gt;There is one&lt;/a&gt; now, but still be careful: it is easy to get burned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating Markdown linters
&lt;/h2&gt;

&lt;p&gt;Reimplementing the Markdown &lt;a href="https://github.com/palkan/docs-example/blob/master/.circleci/config.yml" rel="noopener noreferrer"&gt;linting configuration&lt;/a&gt; I've already had for CircleCI was a pretty straightforward task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lint Docs&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md"&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md"&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;markdownlint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Ruby &lt;/span&gt;&lt;span class="m"&gt;2.6&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-ruby@v1&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.6.x&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Markdown linter&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;gem install mdl&lt;/span&gt;
        &lt;span class="s"&gt;mdl docs&lt;/span&gt;
  &lt;span class="na"&gt;rubocop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Ruby &lt;/span&gt;&lt;span class="m"&gt;2.6&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-ruby@v1&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.6.x&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lint Markdown files with RuboCop&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;gem install bundler&lt;/span&gt;
        &lt;span class="s"&gt;bundle install --gemfile gemfiles/rubocop.gemfile --jobs 4 --retry 3&lt;/span&gt;
        &lt;span class="s"&gt;bundle exec --gemfile gemfiles/rubocop.gemfile rubocop -c .rubocop-md.yml&lt;/span&gt;
  &lt;span class="na"&gt;forspell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Hunspell&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;sudo apt-get install hunspell&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Ruby &lt;/span&gt;&lt;span class="m"&gt;2.6&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-ruby@v1&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.6.x&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Forspell&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;gem install forspell&lt;/span&gt;
        &lt;span class="s"&gt;forspell docs/&lt;/span&gt;
  &lt;span class="na"&gt;liche&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Go&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-go@v1&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;go-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.12.x&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run liche&lt;/span&gt;
      &lt;span class="c1"&gt;# see https://github.com/actions/setup-go/issues/14&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;export PATH=$PATH:$(go env GOPATH)/bin&lt;/span&gt;
        &lt;span class="s"&gt;go get -u github.com/raviqqe/liche&lt;/span&gt;
        &lt;span class="s"&gt;liche -r docs -d docs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few noticeable differences compared to a &lt;a href="https://github.com/palkan/docs-example/blob/master/.circleci/config.yml" rel="noopener noreferrer"&gt;CircleCI config&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ability to trigger actions &lt;em&gt;only when matching files have changed&lt;/em&gt; (See the &lt;code&gt;paths: ["**/*.md"]&lt;/code&gt; declaration).&lt;/li&gt;
&lt;li&gt;Inability to share the setup (checkout, dependency installation) between the jobs from the same action; &lt;strong&gt;there is &lt;a href="https://stackoverflow.com/questions/55110729/how-do-i-cache-steps-in-github-action" rel="noopener noreferrer"&gt;no cache&lt;/a&gt;&lt;/strong&gt;, or anything akin to CircleCI "workspaces".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Adding Neo and Trinity for RSpec
&lt;/h2&gt;

&lt;p&gt;Adding the "Lint Docs" action went smoothly, so I decided to continue with migrating RSpec tests.&lt;/p&gt;

&lt;p&gt;I'm running gems tests against multiple Ruby and frameworks versions to make sure most of the users are covered. I've been doing this successfully for years with Travis' &lt;a href="https://docs.travis-ci.com/user/build-matrix/" rel="noopener noreferrer"&gt;Build Matrix&lt;/a&gt; feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;fast_finish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.6.3&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/railsmaster.gemfile&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jruby-9.2.7.0&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/jruby.gemfile&lt;/span&gt; 
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.6.3&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/activerecord6.gemfile&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.6.3&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gemfile&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.5.3&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gemfile&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.4.2&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/default_factory_girl.gemfile&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.4.2&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/rspec35.gemfile&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.4.2&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/activerecord42.gemfile&lt;/span&gt;
   &lt;span class="na"&gt;allow_failures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.6.2&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/railsmaster.gemfile&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jruby-9.2.7.0&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/jruby.gemfile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub Actions have a similar feature—&lt;a href="https://help.github.com/en/articles/workflow-syntax-for-github-actions#jobsjob_idstrategy" rel="noopener noreferrer"&gt;&lt;code&gt;strategy.matrix&lt;/code&gt;&lt;/a&gt;. And it's even stated in the documentation that the &lt;code&gt;include&lt;/code&gt;-only variant works the same way as in Travis. However, that's &lt;a href="https://github.community/t5/How-to-use-Git-and-GitHub/GitHub-Actions-Matrix-options-dont-work-as-documented/td-p/29558" rel="noopener noreferrer"&gt;not exactly true yet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So, I had to take a detour and use the &lt;code&gt;exclude&lt;/code&gt; option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.5.x"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.6.x"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.4.x"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;
       &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/railsmaster.gemfile"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
       &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/activerecord6.gemfile"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
       &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/activerecord42.gemfile"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
       &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/default_factory_girl.gemfile"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
       &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/rspec35.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.6.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/activerecord42.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.6.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/rspec35.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.6.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/default_factory_girl.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.5.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/railsmaster.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.5.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/activerecord42.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.5.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/rspec35.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.5.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/default_factory_girl.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.4.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/railsmaster.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.4.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/activerecord6.gemfile"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration works exactly the same as the one from Travis (except the missing JRuby, we'll discuss it later), but unfortunately is much less readable.&lt;/p&gt;

&lt;p&gt;Another thing I'd like to point out is that with GitHub Actions (compared to Travis) you have to deal with setting up a correct Gemfile yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure Gemfile&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;bundle config --global gemfile ${{ matrix.gemfile }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, there is no &lt;code&gt;allow_failures&lt;/code&gt; option. There is a &lt;a href="https://help.github.com/en/articles/workflow-syntax-for-github-actions#jobsjob_idstepscontinue-on-error" rel="noopener noreferrer"&gt;&lt;code&gt;steps.continue-on-failure&lt;/code&gt;&lt;/a&gt; toggle which could be used to achieve something similar, but with the strategy matrix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dealing with JRuby
&lt;/h3&gt;

&lt;p&gt;You cannot use JRuby with the &lt;code&gt;actions/setup-ruby&lt;/code&gt; action (or you can, but I couldn't find how?). It requires special treatment.&lt;/p&gt;

&lt;p&gt;Here I applied another GitHub Actions feature—ability to use Docker containers to perform actions. That makes it works very similar to CircleCI.&lt;/p&gt;

&lt;p&gt;I took the &lt;a href="https://hub.docker.com/_/jruby" rel="noopener noreferrer"&gt;official JRuby image&lt;/a&gt; and added a separate job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;rspec-jruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
  &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jruby:9.2.8&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;BUNDLE_GEMFILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/jruby.gemfile&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
  &lt;span class="c1"&gt;# I need git 'cause I use `git ls-files` in my .gemspec&lt;/span&gt;
  &lt;span class="c1"&gt;# to generate the list of the gem's files&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install git&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;apt-get update&lt;/span&gt;
      &lt;span class="s"&gt;apt-get install -y --no-install-recommends git&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install deps and run RSpec&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;gem install bundler&lt;/span&gt;
      &lt;span class="s"&gt;bundle install --jobs 4 --retry 3&lt;/span&gt;
      &lt;span class="s"&gt;bundle exec rspec&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it just works!&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: multiple badges
&lt;/h2&gt;

&lt;p&gt;After merging &lt;a href="https://github.com/palkan/test-prof/pull/156" rel="noopener noreferrer"&gt;the PR&lt;/a&gt; I've started looking for a way to add a build status badge to Readme (since I remove the old one from Travis).&lt;/p&gt;

&lt;p&gt;The answer was found on &lt;a href="https://www.reddit.com/r/github/comments/csehoc/github_actions_official_status_badges/" rel="noopener noreferrer"&gt;Reddit&lt;/a&gt; pretty soon: &lt;code&gt;https://github.com/{github_id}/{repository}/workflows/{workflow_name}/badge.svg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What's interesting here is that you have a separate badge for each workflow. That means that, for example, you shouldn't be afraid of a "red" status due to the yet-another minor RuboCop release.&lt;/p&gt;

&lt;p&gt;Another useful application of this "feature" is an ability to show that your library supports specific runtimes. For example, I split &lt;code&gt;rspec&lt;/code&gt; workflow into two parts: one for different MRI version and another for JRuby. Now it's clear from the README that TestProf has been tested on JRuby and it's (hopefully) green!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Frljeqyj1f935jkd54ro7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Frljeqyj1f935jkd54ro7.png" alt="Badges"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;GitHub Actions look very promising, especially for open source projects. The lack of some features (e.g., caching) would stop from using it as the primary CI/CD tool for commercial projects for now.&lt;/p&gt;

&lt;p&gt;But that is just the beginning; we will see what's coming in the final release!&lt;/p&gt;




&lt;p&gt;Read more dev articles on &lt;a href="https://evilmartians.com/chronicles" rel="noopener noreferrer"&gt;https://evilmartians.com/chronicles&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>github</category>
      <category>ruby</category>
      <category>ci</category>
    </item>
    <item>
      <title>Keeping OSS documentation with Docsify, Lefthook, and friends</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Mon, 02 Sep 2019 18:28:51 +0000</pubDate>
      <link>https://dev.to/evilmartians/keeping-oss-documentation-with-docsify-lefthook-and-friends-11e5</link>
      <guid>https://dev.to/evilmartians/keeping-oss-documentation-with-docsify-lefthook-and-friends-11e5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;"A program is only as good as its documentation."—&lt;a href="https://en.wikipedia.org/wiki/Joe_Armstrong_(programmer)"&gt;Joe Armstrong&lt;/a&gt;, the author of the Erlang programming language&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What makes a good open source project? If you are into Ruby, you can check out all the best practices in one place at &lt;a href="https://gemcheck.evilmartians.io"&gt;Gem Check&lt;/a&gt; (created by yours truly). But even regardless the language or the stack—one thing is vital for all OSS projects: documentation.&lt;/p&gt;


&lt;blockquote class="ltag__twitter-tweet"&gt;
      &lt;div class="ltag__twitter-tweet__media ltag__twitter-tweet__media__video-wrapper"&gt;
        &lt;div class="ltag__twitter-tweet__media--video-preview"&gt;
          &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--k5i619Xi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/tweet_video_thumb/DHy7V8WVoAE2jt8.jpg" alt="unknown tweet media content"&gt;
          &lt;img src="/assets/play-butt.svg" class="ltag__twitter-tweet__play-butt" alt="Play butt"&gt;
        &lt;/div&gt;
        &lt;div class="ltag__twitter-tweet__video"&gt;
          
            
          
        &lt;/div&gt;
      &lt;/div&gt;

  &lt;div class="ltag__twitter-tweet__main"&gt;
    &lt;div class="ltag__twitter-tweet__header"&gt;
      &lt;img class="ltag__twitter-tweet__profile-image" src="https://res.cloudinary.com/practicaldev/image/fetch/s--lVAr5QzI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/profile_images/761377629056307200/AOB3guut_normal.jpg" alt="Kelsey Hightower profile image"&gt;
      &lt;div class="ltag__twitter-tweet__full-name"&gt;
        Kelsey Hightower
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__username"&gt;
        @kelseyhightower
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__twitter-logo"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--P4t6ys1m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/twitter-f95605061196010f91e64806688390eb1a4dbc9e913682e043eb8b1e06ca484f.svg" alt="twitter logo"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__body"&gt;
      So you built it. Nobody's using it. Did you forget the docs? Aha! 
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__date"&gt;
      01:08 AM - 22 Aug 2017
    &lt;/div&gt;


    &lt;div class="ltag__twitter-tweet__actions"&gt;
      &lt;a href="https://twitter.com/intent/tweet?in_reply_to=899800333462806529" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-reply-action.svg" alt="Twitter reply action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/retweet?tweet_id=899800333462806529" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-retweet-action.svg" alt="Twitter retweet action"&gt;
      &lt;/a&gt;
      130
      &lt;a href="https://twitter.com/intent/like?tweet_id=899800333462806529" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-like-action.svg" alt="Twitter like action"&gt;
      &lt;/a&gt;
      440
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/blockquote&gt;


&lt;p&gt;All open source projects, independently of the size, &lt;em&gt;must&lt;/em&gt; be documented (even the now-deprecated, infamous &lt;a href="https://github.com/left-pad/left-pad"&gt;left-pad&lt;/a&gt; is documented). In most cases, a well-crafted README is more than enough, but you can go further and create an &lt;a href="https://github.com/matiassingers/awesome-readme"&gt;"awesome readme"&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, as the project grows, README-backed documentation stops working. We might say "&lt;del&gt;Rails&lt;/del&gt; README doesn't scale!". And we will be right.&lt;/p&gt;

&lt;p&gt;This brings us to a question: &lt;em&gt;"What scales better than a single markdown page?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this text, I am sharing my answer to that.&lt;/p&gt;

&lt;h2&gt;
  
  
  In the Beginning, There Was Chaos
&lt;/h2&gt;

&lt;p&gt;Before we start talking about &lt;a href="https://docsify.js.org/"&gt;docsify&lt;/a&gt;, let me show you some other tools I used, before settling on &lt;em&gt;the one and only&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Wiki
&lt;/h3&gt;

&lt;p&gt;My first project to outgrow its README was &lt;a href="https://anycable.io"&gt;AnyCable&lt;/a&gt;. Without giving it much thought, I started moving bits of documentation into the GitHub's built-in wiki (it's still &lt;a href="https://github.com/anycable/anycable/wiki"&gt;there&lt;/a&gt;, for older versions of the gem). If we already have the tool—that's the way to go, right? Well, not necessarily.&lt;/p&gt;

&lt;p&gt;It turned out that the fact that GitHub wiki is built-in is the only advantage, while the list of cons drags on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Updating code and docs independently is likely to lead to inconsistency.&lt;/li&gt;
&lt;li&gt;Web editor has much to be desired; you cannot easily upload images, for example (technically it is possible, but it requires &lt;a href="https://gist.github.com/subfuzion/0d3f19c4f780a7d75ba2"&gt;cloning the wiki repo&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Cross-references are easy to break just by changing a title; there is no way to have a permanent URL (or I was missing something).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;docs&lt;/code&gt; folder
&lt;/h3&gt;

&lt;p&gt;One way to resolve all the Wiki weak points (see the pun?) at once is to create a &lt;code&gt;docs&lt;/code&gt; folder in the repo and populate it with markdown files. GitHub displays &lt;code&gt;.md&lt;/code&gt; files with formatting when you open them in your browser. And you can also edit the contents right from the web UI! If that's the case, why use the wiki feature at all?&lt;/p&gt;

&lt;p&gt;So, we found a good way to store the documentation contents. But what about the UI/UX? Don't we want to make our documentation more user-friendly (for example, add searching functionality)? Yes, we do.&lt;/p&gt;

&lt;p&gt;Let's convert our GitHub-driven docs to web format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jekyll &amp;amp; GitHub Pages
&lt;/h3&gt;

&lt;p&gt;GitHub helps with setting up a documentation website backed by &lt;a href="https://jekyllrb.com"&gt;Jekyll&lt;/a&gt; in a few clicks: go to "Settings" -&amp;gt; "GitHub Pages," choose the source for the website (we use the &lt;code&gt;docs&lt;/code&gt; folder) and pick a Jekyll theme to your liking.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nlZT9lFh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/l0qmpr7uz1kwazegguez.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nlZT9lFh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/l0qmpr7uz1kwazegguez.png" alt="GitHub Pages settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now visit the URL from the Settings page and find your brand new documentation website there!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--pabDUmAi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/e8bvf2ckgga42lb4glwo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pabDUmAi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/e8bvf2ckgga42lb4glwo.png" alt="Jekyll Website Example"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately, the GitHub Pages Jekyll integration is limited, especially in terms of &lt;a href="https://help.github.com/en/articles/configuring-jekyll-plugins"&gt;the available plugins&lt;/a&gt;. You cannot go far with it. And, in my opinion, Jekyll is a bit too complicated if you want to customize the looks of your page or add some interactivity.&lt;/p&gt;

&lt;p&gt;Let's check out &lt;em&gt;modern&lt;/em&gt; tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docusaurus
&lt;/h3&gt;

&lt;p&gt;The first modern documentation generator I have tried was &lt;a href="https://docusaurus.io"&gt;Docusaurus&lt;/a&gt;. We built the &lt;a href="https://clowne.evilmartians.io"&gt;Clowne&lt;/a&gt;'s gem documentation with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FXWJeASg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/rcgxyj4tdcxadxtek455.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FXWJeASg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/rcgxyj4tdcxadxtek455.png" alt="Clowne Documentation"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But I have to admit that the experience was not so pleasant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Given the fact that the library is built with React, I expected it to have more straightforward customization options. However, you don't really get access to internals and library authors don't want you to do the serious tweaking.&lt;/li&gt;
&lt;li&gt;At the time of my first usage, it did not have the support for live reload, which is crucial for local development (now it seems to be &lt;a href="https://github.com/facebook/Docusaurus/issues/234"&gt;fixed&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;It requires a separate building step (&lt;code&gt;yarn run build&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I started looking for other options and found &lt;a href="https://docsify.js.org/"&gt;docsify&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docsifying documentation
&lt;/h2&gt;

&lt;p&gt;Docsify uses a different approach, compared to Jekyll or Docusaurus, to "generating" a website: it &lt;em&gt;renders markdown files on the fly&lt;/em&gt;, and does not require a build phase.&lt;/p&gt;

&lt;p&gt;There is also support for &lt;a href="https://docsify.js.org/#/ssr"&gt;Server-side rendering&lt;/a&gt; and even for &lt;a href="https://docsify.js.org/#/pwa"&gt;offline mode&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To "docsify" your &lt;code&gt;docs&lt;/code&gt;, you need to do the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;docs/.nojekyll&lt;/code&gt; file to disable Jekyll.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;index.html&lt;/code&gt; that loads and configure docsify:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;http-equiv=&lt;/span&gt;&lt;span class="s"&gt;"X-UA-Compatible"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"IE=edge,chrome=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width,initial-scale=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"//unpkg.com/docsify/themes/vue.css"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$docsify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;loadSidebar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;subMaxLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;palkan/docs-example&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;basePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/docs-example/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;auto2top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;homepage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://raw.githubusercontent.com/palkan/docs-example/master/README.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"//unpkg.com/docsify/lib/docsify.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"//unpkg.com/prismjs/components/prism-bash.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"//unpkg.com/prismjs/components/prism-ruby.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And that is it! Now you have something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--D4nnoErV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/efyauumzlep7586ityac.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--D4nnoErV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/efyauumzlep7586ityac.png" alt="Simple Docsify example"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that I have added some specific configuration options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;basePath: '/docs-example/&lt;/code&gt; defines the root path of your website (which is the repo name for personal projects on GitHub Pages);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;homepage: '...'&lt;/code&gt; is set to the repo's README (by default docsify uses the &lt;code&gt;docs/README.md&lt;/code&gt; file); that allows us to keep both home pages (GitHub and web) in sync.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that's just the beginning! One of the main advantages of docsify is the simplicity of adding useful features via &lt;a href="https://docsify.js.org/#/plugins"&gt;plugins&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let's add a searching functionality.&lt;/p&gt;

&lt;p&gt;All we need is to add these two lines of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight diff"&gt;&lt;code&gt; window.$docsify = {
   loadSidebar: true,
   subMaxLevel: 2,
&lt;span class="gi"&gt;+  search: 'auto',
&lt;/span&gt;   repo: 'palkan/docs-example',
   basePath: '/docs-example/',
   auto2top: true,
   homepage: 'https://raw.githubusercontent.com/palkan/docs-example/master/README.md'
   }
 &amp;lt;/script&amp;gt;
 &amp;lt;script src="//unpkg.com/docsify/lib/docsify.min.js"&amp;gt;&amp;lt;/script&amp;gt;
 &amp;lt;script src="//unpkg.com/docsify/lib/plugins/search.min.js"&amp;gt;&amp;lt;/script&amp;gt;
 &amp;lt;script src="//unpkg.com/prismjs/components/prism-bash.min.js"&amp;gt;&amp;lt;/script&amp;gt;
 &amp;lt;script src="//unpkg.com/prismjs/components/prism-ruby.min.js"&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And &lt;em&gt;voilà&lt;/em&gt;! We can search through our documentation! The searching is implemented on the client side and is backed by indexes saved to &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--p-6fIvcZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/jnk8cljp5uceazqzj6zq.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--p-6fIvcZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/jnk8cljp5uceazqzj6zq.gif" alt="docsify search demo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another thing I like about docsify is the ease of customizing styles: the library uses &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties"&gt;CSS properties&lt;/a&gt;, thus making it possible to change colors and layouts without building your own CSS!&lt;/p&gt;

&lt;p&gt;You can change the color and font sizes right in your &lt;code&gt;index.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;--theme-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ff5e5e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--theme-color-light&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fd7373&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--theme-color-dark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f64242&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--theme-color-secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ff5e5e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--theme-color-secondary-dark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f64242&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--theme-color-secondary-light&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fd7373&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--text-color-base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#363636&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--text-color-secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#646473&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nldQPl-5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/f84i794w13iczmy8w97i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nldQPl-5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/f84i794w13iczmy8w97i.png" alt="Adding red styles"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Isn't it &lt;a href="https://docsify.js.org/#/awesome"&gt;awesome&lt;/a&gt;?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The code for the example above could be found on GitHub: &lt;a href="https://github.com/palkan/docs-example"&gt;palkan/docs-example&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Check out &lt;a href="https://docs.anycable.io"&gt;AnyCable documentation&lt;/a&gt; website and &lt;a href="https://github.com/anycable/docs.anycable.io"&gt;the corresponding repo&lt;/a&gt; for a more advanced example!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--unLB2KcL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/xzqpu3ukix9v9vnxulku.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--unLB2KcL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/xzqpu3ukix9v9vnxulku.png" alt="AnyCable docs"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bonus&lt;/em&gt;*: you can find the implementation of the floating action button for "Edit on GitHub" functionality in &lt;a href="https://gist.github.com/palkan/f702908151f5822bcf8d5daeb41e2f5f"&gt;this gist&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping docs healthy with linters
&lt;/h2&gt;

&lt;p&gt;In the second part of this tutorial, I would like to share my approach to keeping documentation in a healthy state. And by the "healthy state," I mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Style consistency for source files (Markdown) and code examples (Ruby).&lt;/li&gt;
&lt;li&gt;Correct spelling.&lt;/li&gt;
&lt;li&gt;Valid code examples (from the syntax point of view).&lt;/li&gt;
&lt;li&gt;Valid links (no link should lead to 4xx/5xx).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not surprising that for all of the above there is an open source tool (and sometimes many).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/markdownlint/markdownlint"&gt;Markdownlint&lt;/a&gt; helps me to enforce Markdown files style (there is also &lt;a href="https://github.com/DavidAnson/markdownlint"&gt;a NodeJS version&lt;/a&gt; and a &lt;a href="https://github.com/DavidAnson/vscode-markdownlint"&gt;VS Code plugin&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;I also usually disable a couple of rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Line length (&lt;code&gt;MD013&lt;/code&gt;)—modern editors (such as VS Code) could handle this by wrapping long lines.&lt;/li&gt;
&lt;li&gt;HTML fragments—sometimes Markdown is not enough.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To do that I put a &lt;code&gt;.mdlrc&lt;/code&gt; file in the project's root with the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;rules &lt;span class="s2"&gt;"~MD013"&lt;/span&gt;, &lt;span class="s2"&gt;"~MD033"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;To deal with Ruby syntax I use &lt;a href="https://github.com/rubocop-hq/rubocop"&gt;RuboCop&lt;/a&gt; along with the &lt;a href="https://github.com/rubocop-hq/rubocop-md"&gt;rubocop-md&lt;/a&gt; plugin that I wrote specifically for this task. As a default style configuration, I've recently started using &lt;a href="https://github.com/testdouble/standard"&gt;standard&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To make this setup work you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install &lt;code&gt;standard&lt;/code&gt; and &lt;code&gt;rubocop-md&lt;/code&gt; gems (&lt;code&gt;gem install standard&lt;/code&gt; and &lt;code&gt;gem install rubocop-md&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;.rubocop.yml&lt;/code&gt; with the following contents:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;require&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;standard/cop/semantic_blocks&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rubocop-md&lt;/span&gt;

&lt;span class="na"&gt;inherit_gem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;standard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config/base.yml&lt;/span&gt;

&lt;span class="s"&gt;Standard/SemanticBlocks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Run RuboCop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For spellchecking, there is yet another Ruby tool—&lt;a href="https://github.com/kkuprikov/forspell"&gt;Forspell&lt;/a&gt;. It is a wrapper over a well-known &lt;a href="https://en.wikipedia.org/wiki/Hunspell"&gt;Hunspell&lt;/a&gt; package.&lt;/p&gt;

&lt;p&gt;Due to the number of technical terms, you may see a lot of warnings from Forspell during the first run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;forspell docs/

docs/development/lefthook.md:5: lefthook &lt;span class="o"&gt;(&lt;/span&gt;suggestions: left hook, left-hook, leftmost&lt;span class="o"&gt;)&lt;/span&gt;
docs/development/lefthook.md:9: lefthook &lt;span class="o"&gt;(&lt;/span&gt;suggestions: left hook, left-hook, leftmost&lt;span class="o"&gt;)&lt;/span&gt;
docs/development/lefthook.md:11: Hombrew &lt;span class="o"&gt;(&lt;/span&gt;suggestions: Hombre, Hombres, Hombre w&lt;span class="o"&gt;)&lt;/span&gt;
docs/development/lefthook.md:17: Golang &lt;span class="o"&gt;(&lt;/span&gt;suggestions: Golan, Golan g, Angolan&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;That could be easily fixed by running Forspell with the &lt;code&gt;--gen-dictionary&lt;/code&gt; flag: it generates a &lt;code&gt;forspell.dict&lt;/code&gt; file with all the unknown words. Don't forget to scan this file with your eyes and remove the actual typos.&lt;/p&gt;

&lt;p&gt;Finally, to make sure that our documentation does not have any broken links, I use &lt;a href="https://github.com/raviqqe/liche"&gt;liche&lt;/a&gt;—a link checker for Markdown and HTML written in Go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;liche &lt;span class="nt"&gt;-r&lt;/span&gt; docs/

 ERROR    https://githb.com/palkan/anyway_config
                Dialing to the given TCP address timed out
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Liche lacks some features I wish it had: for example, it does not warn about URLs responding with 404. Nevertheless, I found it a bit better than other existing tools.&lt;/p&gt;

&lt;p&gt;In order to manage all these linters, I use &lt;a href="https://github.com/Arkweid/lefthook"&gt;Lefthook&lt;/a&gt; for local development and CircleCI for pull requests.&lt;/p&gt;

&lt;p&gt;Here is the contents of my &lt;code&gt;lefthook.yml&lt;/code&gt;, a file that stores Lefthook's &lt;a href="https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md"&gt;configuration&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;pre-commit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mdl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mdl {staged_files}&lt;/span&gt;
    &lt;span class="na"&gt;liche&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;liche -r docs&lt;/span&gt;
    &lt;span class="na"&gt;forspell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;forspell {staged_files}&lt;/span&gt;
    &lt;span class="na"&gt;rubocop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rubocop {staged_files}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;CirclCI configuration is a little bit more verbose, but does pretty much the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2.1&lt;/span&gt;

&lt;span class="na"&gt;workflows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;build_and_test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;md_lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;requires&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;links_lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;requires&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;spelling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;requires&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rubocop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;requires&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;

&lt;span class="na"&gt;executors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;golang&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;docker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;circleci/golang:1.12.4-stretch&lt;/span&gt;
  &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;docker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;circleci/ruby:2.5-stretch&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;checkout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;executor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;restore_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;project-source-v1-{{ .Branch }}-{{ .Revision }}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;project-source-v1-{{ .Branch }}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;project-source-v1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;save_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;project-source-v1-{{ .Branch }}-{{ .Revision }}&lt;/span&gt;
          &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.git&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;persist_to_workspace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
          &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
  &lt;span class="na"&gt;md_lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;executor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;attach_workspace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install mdl&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gem install mdl&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Markdown lint&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mdl docs&lt;/span&gt;
  &lt;span class="na"&gt;links_lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;executor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golang&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;attach_workspace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install liche&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go get -u github.com/raviqqe/liche&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check links&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;liche -r docs&lt;/span&gt;
  &lt;span class="na"&gt;spelling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;executor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;attach_workspace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install hunspell&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sudo apt-get install hunspell&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install forspell&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gem install forspell&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check spelling&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;forspell docs/&lt;/span&gt;
  &lt;span class="na"&gt;rubocop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;executor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;attach_workspace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install standard&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gem install standard&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install rubocop-md&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gem install rubocop-md&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check Ruby style&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rubocop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The complete example can be found in the &lt;a href="https://github.com/anycable/docs.anycable.io/"&gt;docs.anycable.io&lt;/a&gt; repo (see PRs &lt;a href="https://github.com/anycable/docs.anycable.io/pull/14"&gt;#14&lt;/a&gt; and &lt;a href="https://github.com/anycable/docs.anycable.io/pull/15"&gt;#15&lt;/a&gt;).&lt;/p&gt;




&lt;p&gt;It took me a while to come with this setup (literally, years). Now I can spin up a new documentation website in minutes. Hope you found this article useful and will consider using a similar approach next time you need web-based docs for your projects.&lt;/p&gt;

&lt;p&gt;Documentation for the win!&lt;/p&gt;




&lt;p&gt;Read more dev articles on &lt;a href="https://evilmartians.com/chronicles"&gt;https://evilmartians.com/chronicles&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>documentation</category>
      <category>docsify</category>
      <category>lefthook</category>
    </item>
    <item>
      <title>Better web video with AV1 codec</title>
      <dc:creator>Andrey Sitnik</dc:creator>
      <pubDate>Tue, 20 Aug 2019 16:46:59 +0000</pubDate>
      <link>https://dev.to/evilmartians/better-web-video-with-av1-codec-52kd</link>
      <guid>https://dev.to/evilmartians/better-web-video-with-av1-codec-52kd</guid>
      <description>&lt;p&gt;Learn how to instantly improve online viewing experience for your users by embracing the new AV1 video format that is already supported by Chrome and Firefox. This short guide will also show how to replace your GIF's with videos, using AV1 or H.264, to make your files twenty to forty times smaller.&lt;/p&gt;

&lt;p&gt;The bets are placed. Both YouTube and Netflix have named &lt;a href="https://en.wikipedia.org/wiki/AV1"&gt;AV1&lt;/a&gt; a video codec for the future: Google's video service is already using it on &lt;a href="https://www.youtube.com/testtube"&gt;TestTube&lt;/a&gt; (new, experimental features for YouTube). Netflix has been calling AV1 &lt;a href="http://www.csimagazine.com/csi/Netflix-AV1-is-our-primary-next-gen-codec.php"&gt;"our primary next-gen codec"&lt;/a&gt; for a while now. At Evil Martians, we have already tried AV1 at &lt;a href="https://evilmartians.com/"&gt;our landing page&lt;/a&gt; and at the landing page of &lt;a href="https://amplifr.com/en/"&gt;Amplifr&lt;/a&gt;. In this article, we will share our experience with a new video format and give step-by-step instructions for optimal encoding strategies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Codecs and Containers
&lt;/h2&gt;

&lt;p&gt;With static images, you don't have to think twice: opt for &lt;a href="https://en.wikipedia.org/wiki/JPEG"&gt;JPEG&lt;/a&gt; or &lt;a href="https://en.wikipedia.org/wiki/Portable_Network_Graphics"&gt;PNG&lt;/a&gt; supported by all browsers, or experiment with a more compact Google-developed &lt;a href="https://en.wikipedia.org/wiki/WebP"&gt;WebP&lt;/a&gt; for &lt;a href="https://caniuse.com/#search=webp"&gt;newer browsers&lt;/a&gt;. You can almost always (barring some &lt;a href="http://www.aerasec.de/security/advisories/decompression-bomb-vulnerability.html"&gt;dirty hacking tricks&lt;/a&gt; that tools like &lt;a href="https://github.com/DarthSim/imgproxy"&gt;imgproxy&lt;/a&gt; can protect you &lt;a href="https://evilmartians.com/chronicles/introducing-imgproxy"&gt;against&lt;/a&gt;) be sure that an image file with &lt;code&gt;.png&lt;/code&gt; extension is, indeed, a PNG.&lt;/p&gt;

&lt;p&gt;With video files, it is a bit more tricky than that. File extensions (&lt;code&gt;.mp4&lt;/code&gt;, &lt;code&gt;.wmv&lt;/code&gt;, &lt;code&gt;.webm&lt;/code&gt; or &lt;code&gt;.mov&lt;/code&gt;) barely represent containers, up to three different formats are used to make a video file happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Video codec:&lt;/strong&gt; determines the compression strategy for your video, this is where the trade-offs are made between quality and quantity. On the web, some popular video codecs are H.264, HEVC, VP9, and now AV1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio codec:&lt;/strong&gt; does the same for audio. If your video does not have sound, you can do without it. Otherwise, the popular choices are MP3, Opus, and AAC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Containers&lt;/strong&gt; store both video (compressed by some video codec) and audio streams (compressed by some audio codec), and can also add extra details like subtitles and meta information. Popular containers are MP4, MOV, WebM.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So, when you see &lt;code&gt;.mp4&lt;/code&gt; extension, the only thing you can be sure about is that the MP4 container had been used to package a file. The choice of codecs depends entirely on a creator: it can be H.264/AAC, or AV1/Opus, or something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet AV1
&lt;/h2&gt;

&lt;p&gt;AV1 is a video &lt;em&gt;codec&lt;/em&gt; that was first released almost a year ago: in March 2018. It is designed to compete with previous codec generations such as HEVC/VP9 and H.264/VP8.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BQRwPEuS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/b6ztu3frsea4o994k1g0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BQRwPEuS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/b6ztu3frsea4o994k1g0.png" alt="Codec generations"&gt;&lt;/a&gt;&lt;/p&gt;
Video codecs generations diagram by Tsahi Levent-Levi (&lt;a href="https://webrtcglossary.com/av1/"&gt;source&lt;/a&gt;)



&lt;p&gt;To get familiar with technologies used in a new generation video code, feel free to read &lt;a href="https://people.xiph.org/~xiphmont/demo/av1/demo1.shtml"&gt;"Introducing AV1"&lt;/a&gt; and &lt;a href="https://hacks.mozilla.org/2018/06/av1-next-generation-video-the-constrained-directional-enhancement-filter/"&gt;"AV1: next generation video"&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With all the low-level trickery involved, AV1 is capable of generating files that are up to &lt;a href="https://code.fb.com/video-engineering/av1-beats-x264-and-libvpx-vp9-in-practical-use-case/"&gt;30-50% smaller&lt;/a&gt; than H.264/VP8 and &lt;a href="https://www.youtube.com/watch?v=k7QHUx09dxo"&gt;up to 30%&lt;/a&gt; smaller than HEVC, even though, due to being still mostly experimental, it has some problems (at the time of this writing):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The encoder is not optimized yet. As a result, encoding is &lt;em&gt;extremely&lt;/em&gt; slow (an &lt;a href="https://github.com/xiph/rav1e"&gt;upcoming encoder&lt;/a&gt; written in Rust attempts to solve this issue). The format is not yet ready for live streaming. However, it is perfectly suitable for web, as your average landing page will usually have a short embedded video that rarely changes.&lt;/li&gt;
&lt;li&gt;While supported by Chrome and Firefox, AV1 lacks implementations for Safari and Edge (although Microsoft already has AV1 support in &lt;a href="https://www.microsoft.com/en-us/p/av1-video-extension-beta/9mvzqvxjbq9v?activetab=pivot%3Aoverviewtab"&gt;early beta&lt;/a&gt;). So you need to have at least two versions of all your videos: AV1 for Chrome/Firefox and H.264 for everything else. Ideally, you should have a third, HEVC version for your Safari users on desktop and mobile and we will show how to prepare all three of those files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The central promise for of AV1 is maintaining high image quality even at low bitrates, thus allowing for smaller files without apparent compression artifacts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bboDMIej--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/s5cj5mjh736rsv7f9z2n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bboDMIej--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/s5cj5mjh736rsv7f9z2n.png" alt="Bitrate comparison between major codecs"&gt;&lt;/a&gt;&lt;/p&gt;
A &lt;a href="https://www.streamingmedia.com/Articles/Editorial/Featured-Articles/AV1-A-First-Look-127133.aspx"&gt;chart&lt;/a&gt; by Jan Ozer plots data rate against the VMAF quality metric. AV1 is a clear winner.



&lt;h2&gt;
  
  
  How to use AV1 right now
&lt;/h2&gt;

&lt;p&gt;Now we are going to show a sequence of steps required to produce the quality video content for the web with AV1. First, you need to choose a container: in theory, it does not matter, but MP4 is recommended and seems to be &lt;a href="https://en.wikipedia.org/wiki/AV1#Supported_container_formats"&gt;the most popular at the moment&lt;/a&gt;. For the audio codec, we will use Opus with AV1 as an &lt;a href="http://opus-codec.org/comparison/"&gt;efficient&lt;/a&gt; and free alternative.&lt;/p&gt;

&lt;p&gt;To ensure the best cross-browser compatibility, we will produce not one, but three files:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For desktop Chrome and Firefox (&lt;a href="https://caniuse.com/#search=av1"&gt;31% of users&lt;/a&gt; as of August 2019): MP4 container with AV1 video codec and Opus audio codec.&lt;/li&gt;
&lt;li&gt;For Safari and Edge (&lt;a href="https://caniuse.com/#feat=hevc"&gt;16% of users&lt;/a&gt;): MP4 with HEVC and AAC.&lt;/li&gt;
&lt;li&gt;For other browsers: a larger file in MP4 container with H.264 for video and AAC for audio.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can also go with just options 1 and 3, you will still ensure that all your users can see the video.&lt;/p&gt;

&lt;p&gt;For conversion, I recommend using &lt;a href="https://www.ffmpeg.org/"&gt;FFmpeg&lt;/a&gt; in a terminal. There are plenty of GUI tools for video compression, but CLI allows for steps that are easily reproducible and can be automated with a script. Make sure that you are using the most recent version of FFmpeg, as versions below 4.1 do not support AV1 in an MP4 container. Here are the steps to install it.&lt;/p&gt;

&lt;p&gt;For Mac:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make sure you have &lt;a href="https://brew.sh/"&gt;Homebrew&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;brew install ffmpeg&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For Linux, we recommend using the latest build of FFmpeg from the official website, as at the time of this writing not all package managers contain most recent, AV1-enabled versions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tar -xf ffmpeg-release-amd64-static.tar.xz&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo cp ffmpeg-4.1-64bit-static/ff* /usr/local/bin/&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For Windows, you can use &lt;a href="https://www.wdiaz.org/how-to-install-ffmpeg-on-windows/"&gt;this guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once &lt;code&gt;ffmpeg&lt;/code&gt; executable is available in your command line, let's generate the H.264 file (to ensure compatibility with older browsers). Since all our files will use MP4 as a container, I will use &lt;code&gt;.av1.mp4&lt;/code&gt;, &lt;code&gt;.hevc.mp4&lt;/code&gt;, and &lt;code&gt;.h264.mp4&lt;/code&gt; file extensions. Here is the command you will need to use (don't worry, we will walk you through all the options in a moment).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Replace SOURCE.mov with a path to your source video file&lt;/span&gt;

ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; SOURCE.mov &lt;span class="nt"&gt;-map_metadata&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a libfdk_aac &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 24 &lt;span class="nt"&gt;-preset&lt;/span&gt; veryslow &lt;span class="nt"&gt;-profile&lt;/span&gt;:v main &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"scale=trunc(iw/2)*2:trunc(ih/2)*2"&lt;/span&gt; video.h264.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now open the resulting &lt;code&gt;video.h264.mp4&lt;/code&gt; file and check your quality. If you are satisfied with the result, but the size still seems too big, try adjusting the &lt;code&gt;-crf&lt;/code&gt; option (try &lt;code&gt;-crf 26&lt;/code&gt; or &lt;code&gt;-crf 28&lt;/code&gt;): it will reduce the file size, but also the quality, so try to find an acceptable trade-off. That process, frankly, is more art than science.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can convert H.264 to AV1 if you don't have a single uncompressed source.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now it is time to generate the AV1 file. The command below will take longer than the one for H.264, but that is to be expected: at the moment, AV1 codec does not use the full power of CPU. That is a curse, but also a blessing: if you are about to encode several files at the same time, it is safe to do so, as multi-threading is currently &lt;a href="https://trac.ffmpeg.org/wiki/Encode/AV1"&gt;not supported by default&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; SOURCE.mov &lt;span class="nt"&gt;-map_metadata&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a libopus &lt;span class="nt"&gt;-c&lt;/span&gt;:v libaom-av1 &lt;span class="nt"&gt;-crf&lt;/span&gt; 34 &lt;span class="nt"&gt;-b&lt;/span&gt;:v 0 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"scale=trunc(iw/2)*2:trunc(ih/2)*2"&lt;/span&gt; &lt;span class="nt"&gt;-strict&lt;/span&gt; experimental video.av1.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Play around with the &lt;code&gt;-crf&lt;/code&gt; setting for an optimal size/quality balance.&lt;/p&gt;

&lt;p&gt;Now the same for HEVC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; SOURCE.mov &lt;span class="nt"&gt;-map_metadata&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a libfdk_aac &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx265 &lt;span class="nt"&gt;-crf&lt;/span&gt; 24 &lt;span class="nt"&gt;-preset&lt;/span&gt; veryslow &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart &lt;span class="nt"&gt;-tag&lt;/span&gt;:v hvc1 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"scale=trunc(iw/2)*2:trunc(ih/2)*2"&lt;/span&gt; video.hevc.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then copy all three resulting files (&lt;code&gt;video.h264.mp4&lt;/code&gt;, &lt;code&gt;video.hevc.mp4&lt;/code&gt;, and &lt;code&gt;video.av1.mp4&lt;/code&gt;) to the root of your web project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understand compression options
&lt;/h3&gt;

&lt;p&gt;For now, the commands above look like black magic spells, but all those keys are used for a purpose. Here is what they do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-i SOURCE.mov&lt;/code&gt; sets the source video file for input. FFmpeg will take video and audio streams from this file, convert them, and pack into a new container.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-map_metadata -1&lt;/code&gt; will remove video metadata (like the name of a tool that was used initially to create a video). Sometimes metadata is useful, but for web development it is rarely the case.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-c:a libopus&lt;/code&gt; or &lt;code&gt;-c:a libfdk_aac&lt;/code&gt; selects an &lt;em&gt;audio&lt;/em&gt; codec.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-c:v libaom-av1&lt;/code&gt; selects a &lt;em&gt;video&lt;/em&gt; codec, a library to compress images into a video stream.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-crf 34&lt;/code&gt; stands for Constant Rate Factor and sets your size/quality balance. Think of it as the quality slider for JPEG, but it goes in the opposite direction (&lt;code&gt;0&lt;/code&gt; stands for best quality and bigger size). CRF scale is different for H.264 and AV1: H.264 goes from 0 to 51, AV1 from 0 to 61. As a result, you will have different CRF ratios for AV1 and H.264. According to &lt;a href="https://code.fb.com/video-engineering/av1-beats-x264-and-libvpx-vp9-in-practical-use-case/"&gt;this guide&lt;/a&gt; by Facebook, here are the optimal mappings between H.264 and AV1 CRF values: 19 → 27, 23 → 33, 27 → 39, 31 → 45, 35 → 51, 39 → 57.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-preset veryslow&lt;/code&gt; forces H.264 and HEVC codecs to generate smaller video file even if it will be much longer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-profile:v main&lt;/code&gt; that we use in our H.264 command selects the video codec &lt;a href="https://superuser.com/questions/489087/what-are-the-differences-between-h-264-profiles"&gt;profile&lt;/a&gt;. We can only use "Main", as our video will not be played in Safari otherwise.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-b:v 0&lt;/code&gt; a sets minimum bitrate to force the constant quality mode in AV1.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-pix_fmt yuv420p&lt;/code&gt; (pixel format) is a trick to reduce the size of a video. Basically, it uses full resolution for brightness and a smaller resolution for color. It is a way to fool a human eye, and you can safely remove this argument if it does not work in your case.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-movflags +faststart&lt;/code&gt; moves the important information to the beginning of the file. It allows browser to start playing video during downloading.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-tag:v hvc1&lt;/code&gt; enables native HEVC video support on Apple operating systems.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2"&lt;/code&gt; is a way to ensure the produced video will always have an &lt;em&gt;even&lt;/em&gt; size (some codecs will only work with sizes like &lt;code&gt;300x200&lt;/code&gt; and &lt;code&gt;302x200&lt;/code&gt;, but not with &lt;code&gt;301x200&lt;/code&gt;). This option tells FFmpeg to scale the source for the closes even resolution. If your video dimensions were even in the first place, it would not do anything.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-strict experimental&lt;/code&gt; option needs to be used for AV1 as AV1 encoder is still experimental.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;video.av1.mp4&lt;/code&gt; sets the name for the output file.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Play nice with browsers
&lt;/h3&gt;

&lt;p&gt;Now you need to make sure that browsers will display the right file depending on whether it is supported or not. Luckily, we can set a &lt;code&gt;type&lt;/code&gt; attribute on a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source"&gt;source element&lt;/a&gt;, and only the supported file will be played. For more &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; tag options, look &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video"&gt;here&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;video&lt;/span&gt; &lt;span class="na"&gt;controls&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"600"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"400"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt;
    &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"video.hevc.mp4"&lt;/span&gt;
    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"video/mp4; codecs=hevc,mp4a.40.2"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt;
    &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"video.av1.mp4"&lt;/span&gt;
    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"video/mp4; codecs=av01.0.05M.08,opus"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt;
    &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"video.h264.mp4"&lt;/span&gt;
    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"video/mp4; codecs=avc1.4D401E,mp4a.40.2"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/video&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source tags work similarly to &lt;code&gt;if...else&lt;/code&gt; statements: the browser will read the list of &lt;code&gt;&amp;lt;source&amp;gt;&lt;/code&gt; tags top to bottom and play the first one with supported video type.&lt;/p&gt;

&lt;p&gt;Type attribute describes a file format: which container (&lt;code&gt;video/mp4&lt;/code&gt; for MP4), video codec (&lt;code&gt;av01.0.05M.08&lt;/code&gt; for AV1, &lt;code&gt;hevc&lt;/code&gt; for HEVC and &lt;code&gt;avc1.4D401E&lt;/code&gt; for H.264), and audio codec (&lt;code&gt;opus&lt;/code&gt; for Opus and &lt;code&gt;mp4a.40.2&lt;/code&gt; for AAC) should be used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: how to convert GIFs to AV1 and H.264
&lt;/h2&gt;

&lt;p&gt;In modern times, using GIF for video fragments is a poor practice. GIFs take 20 to 40 times more space than H.264 or AV1. They also require more CPU and power and drain more battery than modern video formats. If you need short animation sequences on your website in 2019, always opt for video codecs. Luckily, FFmpeg supports GIF files as an input source.&lt;/p&gt;

&lt;p&gt;Here's how to convert your GIF to H.264:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; IMAGE.gif &lt;span class="nt"&gt;-map_metadata&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;-an&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 24 &lt;span class="nt"&gt;-preset&lt;/span&gt; veryslow &lt;span class="nt"&gt;-profile&lt;/span&gt;:v main &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"scale=trunc(iw/2)*2:trunc(ih/2)*2"&lt;/span&gt; animation.h264.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's how to go even further and convert it to AV1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; IMAGE.gif &lt;span class="nt"&gt;-map_metadata&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;-an&lt;/span&gt; opus &lt;span class="nt"&gt;-c&lt;/span&gt;:v libaom-av1 &lt;span class="nt"&gt;-crf&lt;/span&gt; 50 &lt;span class="nt"&gt;-b&lt;/span&gt;:v 0 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"scale=trunc(iw/2)*2:trunc(ih/2)*2"&lt;/span&gt; &lt;span class="nt"&gt;-strict&lt;/span&gt; experimental animation.av1.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can use &lt;code&gt;animation.h264.mp4&lt;/code&gt; and &lt;code&gt;animation.av1.mp4&lt;/code&gt; in our HTML. Just replace &lt;code&gt;VIDEO_WIDTH&lt;/code&gt;, &lt;code&gt;VIDEO_HEIGHT&lt;/code&gt;, and &lt;code&gt;PATH_TO_VIDEO&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;video&lt;/span&gt;
  &lt;span class="na"&gt;autoplay&lt;/span&gt;
  &lt;span class="na"&gt;loop&lt;/span&gt;
  &lt;span class="na"&gt;muted&lt;/span&gt;
  &lt;span class="na"&gt;playsinline&lt;/span&gt;
  &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"VIDEO_WIDTH"&lt;/span&gt;
  &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"VIDEO_HEIGHT"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt;
    &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"PATH_TO_VIDEO/animation.av1.mp4"&lt;/span&gt;
    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"video/mp4; codecs=av01.0.05M.08"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt;
    &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"PATH_TO_VIDEO/animation.h264.mp4"&lt;/span&gt;
    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"video/mp4"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/video&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;autoplay&lt;/code&gt; and &lt;code&gt;loop&lt;/code&gt; attributes will emulate the expected behavior of a GIF: looping an animation after the website was loaded. &lt;code&gt;playsinline&lt;/code&gt; will forbid Safari from opening the video in full-screen mode.&lt;/p&gt;




&lt;p&gt;And that concludes our practical guide!&lt;/p&gt;

&lt;p&gt;Even though AV1 codec is still considered experimental, you can already leverage its high-quality, low-bitrate features for a sizable chunk for your web audience (users with current versions of Chrome and Firefox). Of course, you would not want to leave users for other browsers hanging, but the attributes for &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;source&amp;gt;&lt;/code&gt; tags make implementing this logic easy, and in pure HTML, you don't need to go at length to detect user agents with JavaScript. Mastering a few FFmpeg commands also seems like an easy way to improve the video viewing experience for your visitors. We already use AV1 in production on a couple of our projects and have not encountered any significant problems (except for video compression times, but, again, we are dealing mostly with short static sequences).&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>html</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Fullstaq Ruby: First impressions, and how to migrate your Docker/Kubernetes Ruby apps today</title>
      <dc:creator>Andrey Novikov</dc:creator>
      <pubDate>Wed, 07 Aug 2019 14:15:13 +0000</pubDate>
      <link>https://dev.to/evilmartians/fullstaq-ruby-first-impressions-and-how-to-migrate-your-docker-kubernetes-ruby-apps-today-4fm7</link>
      <guid>https://dev.to/evilmartians/fullstaq-ruby-first-impressions-and-how-to-migrate-your-docker-kubernetes-ruby-apps-today-4fm7</guid>
      <description>&lt;h2&gt;
  
  
  What is Fullstaq Ruby?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://fullstaqruby.org/"&gt;Fullstaq Ruby&lt;/a&gt; is a custom build of standard MRI Ruby interpreter with memory allocator replaced, security patches applied, and more goodies on the way.&lt;/p&gt;

&lt;p&gt;If some old timers are here, they can remember REE–Ruby Enterprise Edition–from ancient times of Ruby 1.8.7 and Ruby on Rails 2.2 (almost ten years ago!). Ah, good ol’ times! You could install it via RVM or Rbenv, and some legacy applications are still running on it or have been just recently migrated. REE has a &lt;a href="https://github.com/rvm/rvm/tree/76f66ed8ad6552ad9730999fb3e706154f942f55/patches/ree"&gt;dozen of different patches&lt;/a&gt; on top of Ruby 1.8.7 to improve performance, reduce memory consumption, adjust obsolete security settings, and so on.&lt;/p&gt;

&lt;p&gt;In MRI 1.9.x most of these problems were solved, and, as it gained adoption, REE became obsolete. But even modern “vanilla” MRI still has quirks that can be fixed relatively easy. The most annoying of them is &lt;a href="https://www.speedshop.co/2017/12/04/malloc-doubles-ruby-memory.html"&gt;memory bloat due to memory fragmentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So it is not at all surprising that &lt;a href="https://www.joyfulbikeshedding.com/"&gt;Hongli Lai&lt;/a&gt;, the creator of REE, have released Fullstaq Ruby. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;REE is dead, long live Fullstaq Ruby!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why do we need it?
&lt;/h2&gt;

&lt;p&gt;At one of our projects at &lt;a href="https://evilmartians.com/"&gt;Evil Martians&lt;/a&gt; we were experiencing severe memory bloat. Our application does a lot of IO, and we have a lot of Sidekiq processes with high concurrency setting (20 threads per process). This setting is optimal from the performance point of view because workers are mostly making requests to different remote APIs, our own database, and caches. But such a high level of concurrency also leads to high memory fragmentation. Our Sidekiq processes eat &lt;em&gt;several gigabytes&lt;/em&gt; of RAM each.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read more about choosing Sidekiq concurrency setting in the &lt;a href="https://us11.campaign-archive.com/?u=1aa0f43522f6d9ef96d1c5d6f&amp;amp;id=997fbd1c2c"&gt;Sidekiq in Practice part 1&lt;/a&gt; by Nate Berkopec.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We have decided to replace our MRI 2.6.3 to Fullstaq Ruby 2.6.3 with &lt;a href="http://jemalloc.net/"&gt;jemalloc&lt;/a&gt; to see how it will perform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now that's the difference!
&lt;/h2&gt;

&lt;p&gt;We tried Fullstaq Ruby on a commercial application that runs in production and serves requests from paying clients around the clock. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;First of all: nothing broke. Zero downtime!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, take a look at these monitoring graphs. Memory bloat of long-running processes has practically gone!&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Web application processes have become very stable in memory consumption (4 times less memory!). Bloat still occurs sporadically, but still the readings show that about 50% less memory is consumed during spikes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gN3xSFNd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/kdku0n5tfx9t4yajc8sr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gN3xSFNd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/kdku0n5tfx9t4yajc8sr.png" alt="web pods memory consumption"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Background job workers (we are using Sidekiq) also lost two thirds of their weight. From 1.5-2 GB before to 500-700 MB after the migration to Fullstaq Ruby.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AzGzJS3i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/t93jdbvzwrgp9n3tlsm9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AzGzJS3i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/t93jdbvzwrgp9n3tlsm9.png" alt="sidekiq pods memory consumption"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There is no noticeable difference in memory consumption for short processes (e.g., cron jobs)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We didn't notice any changes in response times or CPU utilization. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The graphs above prove that memory fragmentation was the reason for high memory consumption.&lt;/p&gt;

&lt;p&gt;And that’s it—quite an improvement for swapping one ruby binary for another, isn’t it?&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternatives?
&lt;/h2&gt;

&lt;p&gt;If jemalloc isn't an option for you or you cannot afford to replace MRI with something else, you can try &lt;code&gt;MALLOC_ARENA_MAX=2&lt;/code&gt; &lt;em&gt;spell&lt;/em&gt; to adjust MRI's standard glibc malloc behavior. Results will be close enough to Fullstaq Ruby to treat them almost as equal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YFs8FjKE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/i4wc99f0xb5b2f39mbix.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YFs8FjKE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/i4wc99f0xb5b2f39mbix.png" alt="Fullstaq Ruby with jemalloc on the left and MRI with two malloc arenas on the right"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In our case, Ruby with limited number of malloc arenas (on the right) consumed about 50-100 MB more memory than Ruby with jemalloc (on the left).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read more and see benchmarks of &lt;code&gt;MALLOC_ARENA_MAX=2&lt;/code&gt; in this post:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag__link"&gt;
  &lt;a href="/palkan_tula" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nT6mAN_f--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--2KOdJPra--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/48217/7985e690-6765-41f5-ad5a-74c44393c25c.jpg" alt="palkan_tula image"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/evilmartians/cables-vs-malloctrim-or-yet-another-ruby-memory-usage-benchmark-3emo" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Cables vs. malloc_trim, or yet another Ruby memory usage benchmark&lt;/h2&gt;
      &lt;h3&gt;Vladimir Dementyev ・ Mar 19 '19 ・ 6 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#ruby&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#benchmarks&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#rails&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;

&lt;/blockquote&gt;

&lt;p&gt;We decided to stick with Fullstaq Ruby.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to install?
&lt;/h2&gt;

&lt;p&gt;At the moment the only way to install Fullstaq Ruby is to use deb or rpm packages (either installing directly or via repositories). But we deploy our app to Kubernetes cluster, so we need a Docker image. As there is no "container edition" available on the official website yet, so let’s build our own image–actually, it is not that hard!&lt;/p&gt;

&lt;p&gt;Let's use Debian 9, as this is the Linux distribution being used by official Ruby Docker image, and define the Ruby version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; debian:stretch-slim&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; RUBY_VERSION=2.6.3-jemalloc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then install prerequisites, add Fullstaq Ruby APT repository, install Ruby itself and cleanup apt caches—all in a single command to reduce the Docker layer size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get dist-upgrade &lt;span class="nt"&gt;--assume-yes&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--assume-yes&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; curl gnupg apt-transport-https ca-certificates &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; curl &lt;span class="nt"&gt;-SLf&lt;/span&gt; https://raw.githubusercontent.com/fullstaq-labs/fullstaq-ruby-server-edition/master/fullstaq-ruby.asc | apt-key add - &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb https://apt.fullstaqruby.org debian-9 main"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/apt/sources.list.d/fullstaq-ruby.list &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get update &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--assume-yes&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; fullstaq-ruby-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RUBY_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get autoremove &lt;span class="nt"&gt;--assume-yes&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-fr&lt;/span&gt; /var/cache/apt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fullstaq Ruby also installs Rbenv as dependency but we don't need it in Docker, so let's add ruby and gems binaries to system &lt;code&gt;$PATH&lt;/code&gt; in the &lt;a href="https://github.com/docker-library/ruby/blob/bffb6ff1fbe37874ed506a15eb1bb7faffca589b/2.6/stretch/slim/Dockerfile#L106-L111"&gt;same way that official Docker image for Ruby does&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; GEM_HOME /usr/local/bundle&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; BUNDLE_PATH="$GEM_HOME" \&lt;/span&gt;
    BUNDLE_SILENCE_ROOT_WARNING=1 \
    BUNDLE_APP_CONFIG="$GEM_HOME" \
    RUBY_VERSION=$RUBY_VERSION \
    LANG=C.UTF-8 LC_ALL=C.UTF-8

&lt;span class="c"&gt;# path recommendation: https://github.com/bundler/bundler/pull/6469#issuecomment-383235438&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH $GEM_HOME/bin:$BUNDLE_PATH/gems/bin:/usr/lib/fullstaq-ruby/versions/${RUBY_VERSION}/bin:$PATH&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; [ "irb" ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;We’ve already built and published this image. You can pull it from &lt;a href="https://quay.io/repository/evl.ms/fullstaq-ruby?tab=tags"&gt;our repository&lt;/a&gt; at quay.io&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull quay.io/evl.ms/fullstaq-ruby:2.6.3-jemalloc-stretch-slim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dockerfiles are available on GitHub: &lt;a href="https://github.com/evilmartians/fullstaq-ruby-docker"&gt;https://github.com/evilmartians/fullstaq-ruby-docker&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Now we can just replace base image in our application Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-ARG RUBY_VERSION=2.6.3
&lt;/span&gt;&lt;span class="gi"&gt;+ARG RUBY_VERSION=2.6.3-jemalloc
&lt;/span&gt;
-FROM ruby:${RUBY_VERSION}-stretch-slim
&lt;span class="gi"&gt;+FROM quay.io/evl.ms/fullstaq-ruby:${RUBY_VERSION}-stretch-slim
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And deploy it to staging and then to production.&lt;/p&gt;

&lt;p&gt;Feel free to do the same!&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Migration is smooth. Just reinstall Ruby and gems, and everything should just work.&lt;/li&gt;
&lt;li&gt;Application servers and background jobs worker processes should reduce memory consumption drastically.&lt;/li&gt;
&lt;li&gt;There is no noticeable difference in memory consumption for short processes (like cron jobs or scripts).&lt;/li&gt;
&lt;li&gt;Performance may slightly improve, but it depends on your workload profile.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ruby</category>
      <category>docker</category>
      <category>performance</category>
      <category>memory</category>
    </item>
    <item>
      <title>Speeding up Go Modules for Docker and CI</title>
      <dc:creator>Sergey Ponomarev</dc:creator>
      <pubDate>Mon, 05 Aug 2019 15:19:53 +0000</pubDate>
      <link>https://dev.to/evilmartians/speeding-up-go-modules-for-docker-and-ci-4ebe</link>
      <guid>https://dev.to/evilmartians/speeding-up-go-modules-for-docker-and-ci-4ebe</guid>
      <description>&lt;p&gt;Finally, the Golang world has a built-in, conventional dependency manager in the ecosystem: Go Modules. What began in Go 1.11 as an opt-in feature has become widely adopted by the community, and we are so close to Go 1.13 when Go Modules will be enabled by default. The delightful dilemma of choosing the “best” tool can be finally resolved.&lt;/p&gt;

&lt;p&gt;I can’t help but mention two features which are very close to my heart:&lt;/p&gt;

&lt;p&gt;– No more &lt;code&gt;$GOPATH&lt;/code&gt; imprisonment! In my years of experience, I’d gotten used to storing everything I work on in &lt;code&gt;~/Projects/&lt;/code&gt; and its subfolders somewhere in the home directory, no matter the programming language. So, being forced to keep my Golang stuff in another specific place and respect SCM url in the path was a real pain, and made routine &lt;code&gt;cd&lt;/code&gt; operations feel like such a chore. No longer an issue!&lt;br&gt;
– No more vendoring! Dependency updates don’t produce enormous PR diffs to read, and repositories are lighter. I can just remove the &lt;code&gt;vendor&lt;/code&gt; folder from my source code and forget about it.&lt;/p&gt;

&lt;p&gt;The migration to Go Modules is &lt;a href="https://github.com/golang/go/wiki/Modules#how-to-use-modules"&gt;pretty simple&lt;/a&gt; and won’t take more than a couple of minutes, especially if you use &lt;a href="https://tip.golang.org/pkg/cmd/go/internal/modconv/?m=all#pkg-variables"&gt;any of the supported package managers&lt;/a&gt; to migrate from.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;go mod init
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;rm &lt;/span&gt;vendor/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;go &lt;span class="nb"&gt;test&lt;/span&gt; ./...
&lt;span class="nv"&gt;$ &lt;/span&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git commit
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;That’s pretty much it!&lt;/p&gt;

&lt;p&gt;With Go Modules your dependencies are not a part of your source code anymore. The toolchain downloads them on its own, keeps modules up-to-date, and caches them locally in &lt;code&gt;$GOPATH/pkg/mod&lt;/code&gt; for future use. That sounds perfect for when all of your processes occur in a stateful environment like a laptop, but what about stateless builds in your CI pipeline or Docker? Every now and again Go will download every item in your dependencies and waste your priceless time. Let’s fix that with some caching!&lt;/p&gt;

&lt;h1&gt;
  
  
  Caching on CI
&lt;/h1&gt;

&lt;p&gt;It’s such a common situation to cache dependencies between builds on CI that some of the services provide a simplified, ecosystem-specific syntax to make it easier. Alas, I haven’t found specific Go Modules caching on popular CIs yet, so let’s do it manually.&lt;/p&gt;

&lt;p&gt;If you use TravisCI, it’s very straightforward. Just add those lines to your &lt;code&gt;.travis.yml&lt;/code&gt; config and you’re all set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;directories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$GOPATH/pkg/mod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Setting up dependency caching on my favorite CircleCI is a little more verbose. Wrap &lt;code&gt;go mod download&lt;/code&gt; or your build step in the code below. Golang will take care of the missing dependencies and CircleCI will cache them between builds relying on the content of the &lt;code&gt;go.sum&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;restore_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go-modules-v1-{{ checksum "go.sum" }}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go-modules-v1&lt;/span&gt;
      &lt;span class="c1"&gt;# get dependencies here with `go mod download` or implicitly &lt;/span&gt;
      &lt;span class="c1"&gt;# with `go build` or `go test`&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;save_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go-modules-v1-{{ checksum "go.sum" }}&lt;/span&gt;
          &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/go/pkg/mod"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Here are the results of boosting of my little project on CircleCI:&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;`go test ./...` =&amp;gt; 00:20s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;After cache warm-up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Restoring Cache =&amp;gt; 00:03s
`go test ./...` =&amp;gt; 00:06s
Saving Cache    =&amp;gt; 00:00s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Not bad at all: 2x faster CI build, and for free.&lt;/p&gt;

&lt;h1&gt;
  
  
  Caching in Docker
&lt;/h1&gt;

&lt;p&gt;There are two completely different use cases for how we use Docker: for the development process to isolate an application and its environment, and for packing production builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  In Development
&lt;/h2&gt;

&lt;p&gt;If you follow Test Driven Development (TDD) caching, Go Modules can significantly increase your development productivity. You definitely know how crucial it is to have as fast a test suite as possible.&lt;/p&gt;

&lt;p&gt;Using Docker Compose, cache your modules in a separate volume, and see the performance boost. I saved 20 seconds. Not that bad for a small change!&lt;/p&gt;

&lt;p&gt;Here is a minimal &lt;code&gt;docker-compose.yml&lt;/code&gt;, simplified for the sake of brevity, with highlighted volumes changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application:0.0.1-development&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/development/Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/app&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go-modules:/go/pkg/mod&lt;/span&gt; &lt;span class="c1"&gt;# Put modules cache into a separate volume&lt;/span&gt;
  &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*app&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*app&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go test ./...&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;go-modules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Define the volume&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  In production
&lt;/h2&gt;

&lt;p&gt;For production builds, we can take advantage of the &lt;a href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache"&gt;layer caching&lt;/a&gt; power. Dependencies change less often than the code itself—let’s make it a separate step in your &lt;code&gt;Dockerfile&lt;/code&gt; before the build phase.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# `FROM` and other prerequisites here skipped for the sake of brevity&lt;/span&gt;

&lt;span class="c"&gt;# Copy `go.mod` for definitions and `go.sum` to invalidate the next layer&lt;/span&gt;
&lt;span class="c"&gt;# in case of a change in the dependencies&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; go.mod go.sum ./&lt;/span&gt;
&lt;span class="c"&gt;# Download dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;go mod download

&lt;span class="c"&gt;# `RUN go build ...` and further steps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h1&gt;
  
  
  In a nutshell
&lt;/h1&gt;

&lt;p&gt;Introducing Go Modules was an exciting moment and a significant relief to the Golang community. It brought us a lot of excellent features we had been waiting a long time for. Don’t hesitate to try Modules if you haven’t yet. It’s pretty easy to migrate to it, but don’t forget to change your CI or Docker settings to avoid downloading overhead and keep your builds blazing fast.&lt;/p&gt;

</description>
      <category>go</category>
      <category>modules</category>
      <category>docker</category>
      <category>ci</category>
    </item>
    <item>
      <title>Ruby on Whales: Dockerizing Ruby and Rails development</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Wed, 24 Jul 2019 19:27:13 +0000</pubDate>
      <link>https://dev.to/evilmartians/ruby-on-whales-dockerizing-ruby-and-rails-development-4dm7</link>
      <guid>https://dev.to/evilmartians/ruby-on-whales-dockerizing-ruby-and-rails-development-4dm7</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally posted in &lt;a href="https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development"&gt;Martian Chronicles&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post is a b-side of my recent RailsConf talk "Terraforming legacy Rails applications" (&lt;a href="https://www.youtube.com/watch?v=-NKpMn6XSjU"&gt;video&lt;/a&gt;, &lt;a href="https://speakerdeck.com/palkan/railsconf-2019-terraforming-legacy-rails-applications"&gt;slides&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this post, I am not going to convince you to switch to Docker for application development (though you can check the &lt;a href="https://www.youtube.com/watch?v=-NKpMn6XSjU"&gt;RailsConf video&lt;/a&gt; for some arguments). My goal is to share the configuration I currently use for Rails projects, and which was born in &lt;del&gt;production&lt;/del&gt; development at &lt;a href="https://evilmartians.com/"&gt;Evil Martians&lt;/a&gt;. Feel free to use it!&lt;/p&gt;

&lt;p&gt;I've started using Docker in my development environment about three years ago (instead of Vagrant which was too heavy for my 4GB RAM laptop). It wasn't all roses since the start, of course—I spent two years trying to find a configuration that is &lt;em&gt;good enough&lt;/em&gt;, suitable not only for myself but also for my team.&lt;/p&gt;

&lt;p&gt;Let me present this config here and explain (almost) every line of it, because we've all had enough of cryptic tutorials that just assume you &lt;em&gt;know stuff&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The source code could be found in the &lt;a href="https://github.com/evilmartians/terraforming-rails/blob/master/examples/dockerdev"&gt;evilmartians/terraforming-rails&lt;/a&gt; repository on GitHub.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We use the following stack in this example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ruby 2.6.3&lt;/li&gt;
&lt;li&gt;PostgreSQL 11&lt;/li&gt;
&lt;li&gt;NodeJS 11 &amp;amp; Yarn (for Webpacker-backed assets compilation)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/evilmartians/terraforming-rails/blob/master/examples/dockerdev/.dockerdev/Dockerfile"&gt;&lt;code&gt;Dockerfile&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Dockerfile&lt;/code&gt; defines the &lt;em&gt;environment&lt;/em&gt; for our Ruby application: this is where we run servers, console (&lt;code&gt;rails c&lt;/code&gt;), tests, Rake tasks, interact with our code in any way &lt;em&gt;as developers&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; RUBY_VERSION&lt;/span&gt;
&lt;span class="c"&gt;# See explanation below&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ruby:$RUBY_VERSION&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; PG_MAJOR&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; NODE_MAJOR&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; BUNDLER_VERSION&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; YARN_VERSION&lt;/span&gt;

&lt;span class="c"&gt;# Add PostgreSQL to sources list&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main'&lt;/span&gt; &lt;span class="nv"&gt;$PG_MAJOR&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/apt/sources.list.d/pgdg.list

&lt;span class="c"&gt;# Add NodeJS to sources list&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://deb.nodesource.com/setup_&lt;span class="nv"&gt;$NODE_MAJOR&lt;/span&gt;.x | bash -

&lt;span class="c"&gt;# Add Yarn to the sources list&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'deb http://dl.yarnpkg.com/debian/ stable main'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/apt/sources.list.d/yarn.list

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
&lt;span class="c"&gt;# We use an external Aptfile for that, stay tuned&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; .dockerdev/Aptfile /tmp/Aptfile&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive apt-get &lt;span class="nt"&gt;-yq&lt;/span&gt; dist-upgrade &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-yq&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    build-essential &lt;span class="se"&gt;\
&lt;/span&gt;    postgresql-client-&lt;span class="nv"&gt;$PG_MAJOR&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    nodejs &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nv"&gt;yarn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$YARN_VERSION&lt;/span&gt;&lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/Aptfile | xargs&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get clean &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt; /tmp/&lt;span class="k"&gt;*&lt;/span&gt; /var/tmp/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 0 /var/log/&lt;span class="k"&gt;*&lt;/span&gt;log

&lt;span class="c"&gt;# Configure bundler and PATH&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; LANG=C.UTF-8 \&lt;/span&gt;
  GEM_HOME=/bundle \
  BUNDLE_JOBS=4 \
  BUNDLE_RETRY=3
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; BUNDLE_PATH $GEM_HOME&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; BUNDLE_APP_CONFIG=$BUNDLE_PATH \&lt;/span&gt;
  BUNDLE_BIN=$BUNDLE_PATH/bin
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH /app/bin:$BUNDLE_BIN:$PATH&lt;/span&gt;

&lt;span class="c"&gt;# Upgrade RubyGems and install required Bundler version&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;gem update &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    gem &lt;span class="nb"&gt;install &lt;/span&gt;bundler:&lt;span class="nv"&gt;$BUNDLER_VERSION&lt;/span&gt;

&lt;span class="c"&gt;# Create a directory for the app code&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /app

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This configuration contains the essentials only and could be used as a starting point. Let me show what we are doing here.&lt;/p&gt;

&lt;p&gt;The first two lines could look a bit strange:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; RUBY_VERSION&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ruby:$RUBY_VERSION&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Why not just &lt;code&gt;FROM ruby:2.6.3&lt;/code&gt;, or whatever Ruby stable version du jour it is? We want to make our environment configurable from the outside using Dockerfile as a sort of a template:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the exact versions of runtime dependencies are specified in the &lt;code&gt;docker-compose.yml&lt;/code&gt; (see below);&lt;/li&gt;
&lt;li&gt;the list of &lt;code&gt;apt&lt;/code&gt;-installable dependencies is stored in a separate file (also see below).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following three lines define arguments for PostgreSQL, NodeJS, Yarn, and Bundler versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; PG_MAJOR&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; NODE_MAJOR&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; BUNDLER_VERSION&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; YARN_VERSION&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Since we do not expect anyone to use this Dockerfile without &lt;a href="https://docs.docker.com/compose/"&gt;Docker Compose&lt;/a&gt;, we do not provide default values.&lt;/p&gt;

&lt;p&gt;Installing PostgreSQL, NodeJS, Yarn via &lt;code&gt;apt&lt;/code&gt; requires adding their deb packages repos to the sources list.&lt;/p&gt;

&lt;p&gt;For PostgreSQL (based in the &lt;a href="https://www.postgresql.org/download/linux/debian/"&gt;official documentation&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main'&lt;/span&gt; &lt;span class="nv"&gt;$PG_MAJOR&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/apt/sources.list.d/pgdg.list
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;For NodeJS (from &lt;a href="https://github.com/nodesource/distributions/blob/master/README.md#debinstall"&gt;NodeSource repo&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://deb.nodesource.com/setup_&lt;span class="nv"&gt;$NODE_MAJOR&lt;/span&gt;.x | bash -
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;For Yarn (from the &lt;a href="https://yarnpkg.com/en/docs/install#debian-stable"&gt;official website&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'deb http://dl.yarnpkg.com/debian/ stable main'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/apt/sources.list.d/yarn.list
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Now it's time to install the dependencies, i.e. run &lt;code&gt;apt-get install&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; .dockerdev/Aptfile /tmp/Aptfile&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive apt-get &lt;span class="nt"&gt;-yq&lt;/span&gt; dist-upgrade &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-yq&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    build-essential &lt;span class="se"&gt;\
&lt;/span&gt;    postgresql-client-&lt;span class="nv"&gt;$PG_MAJOR&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    nodejs &lt;span class="se"&gt;\
&lt;/span&gt;    yarn &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/Aptfile | xargs&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get clean &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt; /tmp/&lt;span class="k"&gt;*&lt;/span&gt; /var/tmp/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 0 /var/log/&lt;span class="k"&gt;*&lt;/span&gt;log
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;First, let's talk about the Aptfile trick:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; .dockerdev/Aptfile /tmp/Aptfile&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/Aptfile | xargs&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;I borrowed this idea from &lt;a href="https://github.com/heroku/heroku-buildpack-apt"&gt;heroku-buildpack-apt&lt;/a&gt;, which allows installing additional packages on Heroku. If you're using this buildpack, you can even re-use the same Aptfile for local and production environment (though the buildpack's one provides more functionality).&lt;/p&gt;

&lt;p&gt;Our &lt;a href="https://github.com/evilmartians/terraforming-rails/blob/master/examples/dockerdev/.dockerdev/Aptfile"&gt;default Aptfile&lt;/a&gt; contains only a single package (we use Vim to edit Rails Credentials):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;vim
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In one of the previous project I worked on, we generated PDFs using LaTeX and &lt;a href="https://www.tug.org/texlive/"&gt;TexLive&lt;/a&gt;. Our Aptfile might look like this (those days I didn't use this trick):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;vim
texlive
texlive-latex-recommended
texlive-fonts-recommended
texlive-lang-cyrillic
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This way, we keep the task-specific dependencies in a separate file, making our Dockerfile more universal.&lt;/p&gt;

&lt;p&gt;With regards to &lt;code&gt;DEBIAN_FRONTEND=noninteractive&lt;/code&gt;, I kindly ask you to take a look at &lt;a href="https://askubuntu.com/a/972528"&gt;answer on Ask Ubuntu&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--no-install-recommends&lt;/code&gt; switch helps us to save some space (and make our image slimmer) by not installing recommended packages. See more &lt;a href="http://xubuntugeek.blogspot.com/2012/06/save-disk-space-with-apt-get-option-no.html"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The last part of this &lt;code&gt;RUN&lt;/code&gt; (&lt;code&gt;apt-get clean &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* &amp;amp;&amp;amp; truncate -s 0 /var/log/*log&lt;/code&gt;) also serves the same purpose—clears out the local repository of retrieved package files (we installed everything, we don't need them anymore) and all the temporary files and logs created during the installation. We need this cleanup to be in the same &lt;code&gt;RUN&lt;/code&gt; statement to make sure this particular &lt;a href="https://docs.docker.com/storage/storagedriver/#images-and-layers"&gt;Docker layer&lt;/a&gt; doesn't contain any garbage.&lt;/p&gt;

&lt;p&gt;The final part is mostly devoted to Bundler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; LANG=C.UTF-8 \&lt;/span&gt;
  GEM_HOME=/bundle \
  BUNDLE_JOBS=4 \
  BUNDLE_RETRY=3
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; BUNDLE_PATH $GEM_HOME&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; BUNDLE_APP_CONFIG=$BUNDLE_PATH \&lt;/span&gt;
  BUNDLE_BIN=$BUNDLE_PATH/bin
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH /app/bin:$BUNDLE_BIN:$PATH&lt;/span&gt;

&lt;span class="c"&gt;# Upgrade RubyGems and install required Bundler version&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;gem update &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    gem &lt;span class="nb"&gt;install &lt;/span&gt;bundler:&lt;span class="nv"&gt;$BUNDLER_VERSION&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;LANG=C.UTF-8&lt;/code&gt; sets the default locale to UTF-8. Otherwise Ruby uses US-ASCII for strings and bye-bye those sweet sweet emojis 👋&lt;/p&gt;

&lt;p&gt;We set the path for gem installations via &lt;code&gt;GEM_HOME=/bundle&lt;/code&gt;. What is &lt;code&gt;/bundle&lt;/code&gt;? That's the path where we're going to mount as a &lt;em&gt;volume&lt;/em&gt; later to persist the dependencies on the host system, i.e., your development machine (see below in &lt;code&gt;docker-compose.yml&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;BUNDLE_PATH&lt;/code&gt; and &lt;code&gt;BUNDLE_BIN&lt;/code&gt; variables tell Bundler where to look for gems and Ruby executables.&lt;/p&gt;

&lt;p&gt;Finally, we expose Ruby and application binaries globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH /app/bin:$BUNDLE_BIN:$PATH&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;That allows us to run &lt;code&gt;rails&lt;/code&gt;, &lt;code&gt;rake&lt;/code&gt;, &lt;code&gt;rspec&lt;/code&gt; and other &lt;em&gt;binstubbed&lt;/em&gt; commands without prefixing them with &lt;code&gt;bundle exec&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/evilmartians/terraforming-rails/blob/master/examples/dockerdev/docker-compose.yml"&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.docker.com/compose/"&gt;Docker Compose&lt;/a&gt; is a tool to orchestrate our containerized environment. It allows us to link containers to each other, define persistent volumes and services.&lt;/p&gt;

&lt;p&gt;Below is the compose file for a typical Rails application development with PostgreSQL as a database, and Sidekiq background job processor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.4'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;app&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.dockerdev/Dockerfile&lt;/span&gt;
      &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;RUBY_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2.6.3'&lt;/span&gt;
        &lt;span class="na"&gt;PG_MAJOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;11'&lt;/span&gt;
        &lt;span class="na"&gt;NODE_MAJOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;11'&lt;/span&gt;
        &lt;span class="na"&gt;YARN_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.13.0'&lt;/span&gt;
        &lt;span class="na"&gt;BUNDLER_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2.0.2'&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example-dev:1.0.0&lt;/span&gt;
    &lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/tmp&lt;/span&gt;

  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;backend&lt;/span&gt;
    &lt;span class="s"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*app&lt;/span&gt;
    &lt;span class="na"&gt;stdin_open&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;tty&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/app:cached&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rails_cache:/app/tmp/cache&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bundle:/bundle&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules:/app/node_modules&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;packs:/app/public/packs&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.dockerdev/.psqlrc:/root/.psqlrc:ro&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_ENV=development&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;RAILS_ENV=${RAILS_ENV:-development}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;REDIS_URL=redis://redis:6379/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL=postgres://postgres:postgres@postgres:5432&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;BOOTSNAP_CACHE_DIR=/bundle/bootsnap&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEBPACKER_DEV_SERVER_HOST=webpacker&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEB_CONCURRENCY=1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HISTFILE=/app/log/.bash_history&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PSQL_HISTFILE=/app/log/.psql_history&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EDITOR=vi&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;

  &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*backend&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/bin/bash&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3000:3000'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3002:3002'&lt;/span&gt;

  &lt;span class="na"&gt;rails&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*backend&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec rails server -b 0.0.0.0&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3000:3000'&lt;/span&gt;

  &lt;span class="na"&gt;sidekiq&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*backend&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec sidekiq -C config/sidekiq.yml&lt;/span&gt;

  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:11.1&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.psqlrc:/root/.psqlrc:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres:/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./log:/root/log:cached&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PSQL_HISTFILE=/root/log/.psql_history&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:3.2-alpine&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis:/data&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;6379&lt;/span&gt;

  &lt;span class="na"&gt;webpacker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*app&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./bin/webpack-dev-server&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3035:3035'&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/app:cached&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bundle:/bundle&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules:/app/node_modules&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;packs:/app/public/packs&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_ENV=${NODE_ENV:-development}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;RAILS_ENV=${RAILS_ENV:-development}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEBPACKER_DEV_SERVER_HOST=0.0.0.0&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;node_modules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rails_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;packs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;We define &lt;strong&gt;eight&lt;/strong&gt; services. Why so many? Some of them only define shared configuration for others (&lt;em&gt;abstract&lt;/em&gt; services, e.g., &lt;code&gt;app&lt;/code&gt; and &lt;code&gt;backend&lt;/code&gt;), others are used to specific commands using the application container (e.g., &lt;code&gt;runner&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;With this approach, we do not use &lt;code&gt;docker-compose up&lt;/code&gt; command to run our application, but always specify the exact service we want to run (e.g., &lt;code&gt;docker-compose up rails&lt;/code&gt;). That makes sense: in development, you rarely need all of the services up and running (Webpacker, Sidekiq, etc.).&lt;/p&gt;

&lt;p&gt;Let's take a thorough look at each service.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;app&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The main purpose of this service is to provide all the required information to build our application container (the one defined in the &lt;code&gt;Dockerfile&lt;/code&gt; above):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
  &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.dockerdev/Dockerfile&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;RUBY_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2.6.3'&lt;/span&gt;
    &lt;span class="na"&gt;PG_MAJOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;11'&lt;/span&gt;
    &lt;span class="na"&gt;NODE_MAJOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;11'&lt;/span&gt;
    &lt;span class="na"&gt;YARN_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.13.0'&lt;/span&gt;
    &lt;span class="na"&gt;BUNDLER_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2.0.2'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;context&lt;/code&gt; directory defines the &lt;a href="https://docs.docker.com/compose/compose-file/#context"&gt;build context&lt;/a&gt; for Docker: this is something like a working directory for the build process, it's used by the &lt;code&gt;COPY&lt;/code&gt; command, for example.&lt;/p&gt;

&lt;p&gt;We explicitly specify the path to Dockerfile since we do not keep it in the project root, packing all Docker-related files inside a hidden &lt;code&gt;.dockerdev&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;And, as we mentioned earlier, we specify the exact version of dependencies using &lt;code&gt;args&lt;/code&gt; declared in the Dockerfile.&lt;/p&gt;

&lt;p&gt;One thing that we should pay attention to is the way we tag images:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example-dev:1.0.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;One of the benefits of using Docker for development is the ability to synchronize the configuration changes across the team automatically. You only need to upgrade the local image version every time you make changes to it (or to the arguments or files it relies on). The worst thing you can do is to use &lt;code&gt;example-dev:latest&lt;/code&gt; as your build tag.&lt;/p&gt;

&lt;p&gt;Keeping an image version also helps to work with two different environments without any additional hassle. For example, when you work on a long-running "chore/upgrade-to-ruby-3" branch, you can easily switch to &lt;code&gt;master&lt;/code&gt; and use the older image with the older Ruby, no need to rebuild anything.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The worst thing you can do is to use &lt;code&gt;latest&lt;/code&gt; tags for images in your &lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We also &lt;em&gt;tell&lt;/em&gt; Docker to &lt;a href="https://docs.docker.com/v17.09/engine/admin/volumes/tmpfs/#choosing-the-tmpfs-or-mount-flag"&gt;use tmpfs&lt;/a&gt; for &lt;code&gt;/tmp&lt;/code&gt; folder within a container to speed things up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/tmp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;backend&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;We reached the most interesting part of this post.&lt;/p&gt;

&lt;p&gt;This service defines the shared behavior of all  Ruby services.&lt;/p&gt;

&lt;p&gt;Let's talk about the volumes first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/app:cached&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bundle:/bundle&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rails_cache:/app/tmp/cache&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules:/app/node_modules&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;packs:/app/public/packs&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.dockerdev/.psqlrc:/root/.psqlrc:ro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The first item in the volumes list mounts the current working directory (the project's root) to the &lt;code&gt;/app&lt;/code&gt; folder within a container using the &lt;code&gt;cached&lt;/code&gt; strategy. This &lt;code&gt;cached&lt;/code&gt; modifier is the key to efficient Docker development on MacOS. We're not going to dig deeper in this post (we're working on a separate one on this subject 😉), but you can take a look at &lt;a href="https://docs.docker.com/docker-for-mac/osxfs-caching/"&gt;the docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The next line &lt;em&gt;tells&lt;/em&gt; our container to use a volume named &lt;code&gt;bundle&lt;/code&gt; to store &lt;code&gt;/bundle&lt;/code&gt; contents. This way we persist our gems data across runs: all the volumes defined in the &lt;code&gt;docker-compose.yml&lt;/code&gt; stay put until we run &lt;code&gt;docker-compose down --volumes&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The following three lines are also there to get rid of the "Docker is slow on Mac" curse. We put all the generated files into Docker volumes to avoid heavy disk operations on the host machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rails_cache:/app/tmp/cache&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules:/app/node_modules&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;packs:/app/public/packs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;To make Docker fast enough on MacOS follow these two rules: use &lt;code&gt;:cached&lt;/code&gt; to mount source files and use volumes for generated content (assets, bundle, etc.).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The last line adds a specific &lt;code&gt;psql&lt;/code&gt; configuration to the container. We mostly need it to persist the commands history by storing it in the app's &lt;code&gt;log/.psql_history&lt;/code&gt; file. Why &lt;code&gt;psql&lt;/code&gt; in the Ruby container? It's used internally when you run &lt;code&gt;rails dbconsole&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Our &lt;a href="https://github.com/evilmartians/terraforming-rails/blob/master/examples/dockerdev/.dockerdev/.psqlrc"&gt;&lt;code&gt;.psqlrc&lt;/code&gt;&lt;/a&gt; file contains the following trick to make it possible to specify the path to the history file via the env variable (allow specifying the path to history file via &lt;code&gt;PSQL_HISTFILE&lt;/code&gt; env variable, and fallback to the defaukt &lt;code&gt;$HOME/.psql_history&lt;/code&gt; otherwise):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;HISTFILE&lt;/span&gt; &lt;span class="nv"&gt;`[[ -z $PSQL_HISTFILE ]] &amp;amp;&amp;amp; echo $HOME/.psql_history || echo $PSQL_HISTFILE`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Let's talk about the environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_ENV=${NODE_ENV:-development}&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;RAILS_ENV=${RAILS_ENV:-development}&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;REDIS_URL=redis://redis:6379/&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL=postgres://postgres:postgres@postgres:5432&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEBPACKER_DEV_SERVER_HOST=webpacker&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;BOOTSNAP_CACHE_DIR=/bundle/bootsnap&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HISTFILE=/app/log/.bash_history&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PSQL_HISTFILE=/app/log/.psql_history&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EDITOR=vi&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MALLOC_ARENA_MAX=2&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEB_CONCURRENCY=${WEB_CONCURRENCY:-1}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;There are several things here, and I'd like to focus one.&lt;/p&gt;

&lt;p&gt;First, the &lt;code&gt;X=${X:-smth}&lt;/code&gt; syntax. It could be translated as "For X variable within the container use the host machine X env variable value if present and another value otherwise". &lt;em&gt;Thus, we make it possible to run a service in a different environment provided along with the command, e.g., &lt;code&gt;RAILS_ENV=test docker-compose up rails&lt;/code&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;DATABASE_URL&lt;/code&gt;, &lt;code&gt;REDIS_URL&lt;/code&gt;, and &lt;code&gt;WEBPACKER_DEV_SERVER_HOST&lt;/code&gt; variables &lt;em&gt;connect&lt;/em&gt; our Ruby application to other services. The &lt;code&gt;DATABASE_URL&lt;/code&gt; and &lt;code&gt;WEBPACKER_DEV_SERVER_HOST&lt;/code&gt; variables are supported by Rails (ActiveRecord and Webpacker respectively) out-of-the-box. Some libraries support &lt;code&gt;REDIS_URL&lt;/code&gt; as well (Sidekiq) but not all of them (for instance, Action Cable must be configured explicitly).&lt;/p&gt;

&lt;p&gt;We use &lt;a href="https://www.github.com/Shopify/bootsnap"&gt;bootsnap&lt;/a&gt; to speed up the application load time. We store its cache in the same volume as the Bundler data because this cache mostly contains the gems data; thus, we should drop everything altogether in case we do another Ruby version upgrade, for instance.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;HISTFILE=/app/log/.bash_history&lt;/code&gt; is the significant setting from the developer's UX point of view: it tells Bash to store its history in the specified location, thus making it persistent.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;EDITOR=vi&lt;/code&gt; is used, for example, by &lt;code&gt;rails credentials:edit&lt;/code&gt; command to manage credentials files.&lt;/p&gt;

&lt;p&gt;Finally, the last two settings, &lt;code&gt;MALLOC_ARENA_MAX&lt;/code&gt; and &lt;code&gt;WEB_CONCURRENCY&lt;/code&gt;, are there to help you keep Rails memory handling in check.&lt;/p&gt;

&lt;p&gt;The only lines in this service yet to cover are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;stdin_open&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;tty&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;They make this service &lt;em&gt;interactive&lt;/em&gt;, i.e., provide a TTY. We need it, for example, to run Rails console or Bash within a container.&lt;/p&gt;

&lt;p&gt;It is the same as running a Docker container with the &lt;code&gt;-it&lt;/code&gt; options.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;webpacker&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The only thing I want to mention here is the &lt;code&gt;WEBPACKER_DEV_SERVER_HOST=0.0.0.0&lt;/code&gt; setting: it makes Webpack dev server accessible from the &lt;em&gt;outside&lt;/em&gt; (by default it runs on &lt;code&gt;localhost&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;runner&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To explain what is this service for, let me share the way I use Docker for development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I start a Docker daemon running a custom &lt;code&gt;docker-start&lt;/code&gt; script:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;docker info &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Opening Docker for Mac..."&lt;/span&gt;
  open &lt;span class="nt"&gt;-a&lt;/span&gt; /Applications/Docker.app
  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; docker system info &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Docker is ready to rock!"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Docker is up and running."&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Then I run &lt;code&gt;dcr runner&lt;/code&gt; (&lt;code&gt;dcr&lt;/code&gt; is an alias for &lt;code&gt;docker-compose run&lt;/code&gt;) in the project directory to log into the container's shell; this is an alias for:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker-compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; runner
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;I run (almost) everything from within this container: tests, migrations, Rake tasks, whatever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As you can see, I do not spin a new container every time I need to run a task, and I'm always using the same one.&lt;/p&gt;

&lt;p&gt;Thus, I'm using &lt;code&gt;dcr runner&lt;/code&gt; the same way I used &lt;code&gt;vagrant ssh&lt;/code&gt; years ago.&lt;/p&gt;

&lt;p&gt;The only reason why it's called &lt;code&gt;runner&lt;/code&gt; and not &lt;code&gt;shell&lt;/code&gt;, for example, is that it also could be used to &lt;em&gt;run&lt;/em&gt; arbitrary commands within a container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The &lt;code&gt;runner&lt;/code&gt; service is a matter of taste, it doesn't bring anything new comparing to the &lt;code&gt;web&lt;/code&gt; service, except from the default &lt;code&gt;command&lt;/code&gt; (&lt;code&gt;/bin/bash&lt;/code&gt;); thus, &lt;code&gt;docker-compose run runner&lt;/code&gt; is exactly the same as &lt;code&gt;docker-compose run web /bin/bash&lt;/code&gt; (but shorter 😉).&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: &lt;a href="https://github.com/evilmartians/terraforming-rails/blob/master/examples/dockerdev/dip.yml"&gt;dip.yml&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;If you still think that the &lt;em&gt;Docker Compose&lt;/em&gt; way is too complicated, there is a tool called &lt;a href="https://github.com/bibendi/dip"&gt;Dip&lt;/a&gt; developed by one of my colleages at Evil Martians that aims to make the developer experience smoother.&lt;/p&gt;

&lt;p&gt;It is especially useful if you have multiple compose files or platform-dependent configurations because it could glue them together and provide a universal interface to manage the Docker development environment.&lt;/p&gt;

&lt;p&gt;We're going to tell you more about it in the future. Stay tuned!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; Special thanks to &lt;a href="https://github.com/sponomarev"&gt;Sergey Ponomarev&lt;/a&gt; and &lt;a href="https://github.com/bibendi"&gt;Mikhail Merkushin&lt;/a&gt; for sharing their tips on the subject. 🤘&lt;/p&gt;




&lt;p&gt;Read more dev articles on &lt;a href="https://evilmartians.com/chronicles"&gt;https://evilmartians.com/chronicles&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>docker</category>
    </item>
    <item>
      <title>Reporting non-nullable violations in graphql-ruby properly</title>
      <dc:creator>Andrey Novikov</dc:creator>
      <pubDate>Fri, 19 Jul 2019 07:37:08 +0000</pubDate>
      <link>https://dev.to/evilmartians/reporting-non-nullable-violations-in-graphql-ruby-properly-2k3b</link>
      <guid>https://dev.to/evilmartians/reporting-non-nullable-violations-in-graphql-ruby-properly-2k3b</guid>
      <description>&lt;p&gt;GraphQL encourages you to decouple your frontend from your backend. You can think more about your types’ “correctness” and less about particular view layout. To achieve that GraphQL provides strong type system. GraphQL doesn’t allow to return String instead of Integer and also doesn’t allow return &lt;code&gt;null&lt;/code&gt;s where API declares that type is &lt;em&gt;non-nullable&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;But sometimes your data will accidentally contain empty values where even you don’t expect them. Some external APIs will return incorrect or missing data, even if its &lt;em&gt;contract&lt;/em&gt; disallows that. Sometimes it will be your own bug. But in any case, it is your responsibility to track for such failures and correct them.&lt;/p&gt;

&lt;p&gt;If you use Ruby for developing GraphQL API, the chances are high that you use graphql-ruby. It is standard de-facto in Ruby because it is a very mature and feature-rich gem with a whole ecosystem of plugins. But “out of the box” there it doesn’t inform you about violations of these “non-nullable type” rules and just silently returns a partial response with error messages to the client.&lt;/p&gt;

&lt;p&gt;But we want to know about non-null violations because it is our responsibility to develop bullet-proof applications. And according to the graphql-ruby’s &lt;a href="https://graphql-ruby.org/errors/type_errors"&gt;documentation about type errors&lt;/a&gt;, there is a special hook to catch such errors. Let’s use it!&lt;/p&gt;

&lt;p&gt;We’re using Honeybadger, so we’ll report this offense there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;YourAppSchema&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;GraphQL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;GraphQL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;InvalidNullError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="no"&gt;Honeybadger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;context: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;query: &lt;/span&gt;&lt;span class="n"&gt;query_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;So far, so good. Soon we’ll able to see our first error in error tracker UI.&lt;/p&gt;

&lt;p&gt;But wait, what’s it? There are errors about non-null violations in multiple different fields mixed in one error field. Aren’t they different bugs? What’s wrong?&lt;/p&gt;

&lt;p&gt;It turns out that by default &lt;a href="https://docs.honeybadger.io/lib/ruby/getting-started/customizing-error-grouping.html"&gt;Honeybadger groups errors together&lt;/a&gt; by exception class, component (controller and action), and backtrace fragment. Please note that the exception message doesn’t count. In the case of GraphQL, it is always a single controller and action, so exceptions for absolutely different queries are getting grouped together. Let’s fix that!&lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://docs.honeybadger.io/lib/ruby/getting-started/customizing-error-grouping.html"&gt;the same documentation page&lt;/a&gt;, we need to calculate unique &lt;em&gt;fingerprint&lt;/em&gt; string for every different error. So, what we need to include to this fingerprint for non-null violation?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exception class? Yes, we need to distinguish &lt;code&gt;GraphQL::InvalidNullError&lt;/code&gt; from other types of errors&lt;/li&gt;
&lt;li&gt;Controller and action? No, they’re always the same. And even if an error will occur during triggering subscription, will it make any difference?&lt;/li&gt;
&lt;li&gt;Backtrace? Hmm… I’m not sure. What do you think?&lt;/li&gt;
&lt;li&gt;Type and field names, of course! We need to separate errors in different fields.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s use an exception class name, GraphQL type name, and field name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationSchema&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;GraphQL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;type_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;fingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;honeybadger_fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="no"&gt;Honeybadger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;fingerprint: &lt;/span&gt;&lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;context: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;query:   &lt;/span&gt;&lt;span class="n"&gt;query_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;user_id: &lt;/span&gt;&lt;span class="n"&gt;query_context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;super&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="kp"&gt;private&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;honeybadger_fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;GraphQL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;InvalidNullError&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parent_type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And that’s it: from now on exceptions for different fields lives in different Honeybadger errors.&lt;/p&gt;

&lt;p&gt;Thank you for your attention! Let’s build reliable APIs!&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>graphql</category>
      <category>errors</category>
      <category>monitoring</category>
    </item>
  </channel>
</rss>
