<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.1.1">Jekyll</generator><link href="/notes/feed.xml" rel="self" type="application/atom+xml" /><link href="/notes/" rel="alternate" type="text/html" /><updated>2023-10-30T15:09:42-07:00</updated><id>/notes/feed.xml</id><title type="html">Alex’s Blag</title><subtitle>Full stack web developer, part-time beatmaker, CTO of Tickit.</subtitle><entry><title type="html">Migrate Your Asset Pipeline to Rails 7.x</title><link href="/notes/2023/10/24/migrating-your-asset-pipeline-to-rails-7x.html" rel="alternate" type="text/html" title="Migrate Your Asset Pipeline to Rails 7.x" /><published>2023-10-24T10:00:00-07:00</published><updated>2023-10-24T10:00:00-07:00</updated><id>/notes/2023/10/24/migrating-your-asset-pipeline-to-rails-7x</id><content type="html" xml:base="/notes/2023/10/24/migrating-your-asset-pipeline-to-rails-7x.html">&lt;p&gt;After another confusing migration of a Rails app’s asset stack into I finally found a stack that I’m happy with for Rails 7.x.&lt;/p&gt;

&lt;h2 id=&quot;before&quot;&gt;Before…&lt;/h2&gt;

&lt;p&gt;I had some Rails 6 and Rails 5 apps. One of them was using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;webpacker&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sprockets 3&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap-sass 3&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;react&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;typescript&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jquery-ui&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jquery-ujs&lt;/code&gt;. Two others were using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sprockets 3&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap-sass 3&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;the-concept&quot;&gt;The Concept&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Use JS-based tools like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node-sass&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;esbuild&lt;/code&gt; to compile CSS and JS files and output the compiled files to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/assets/builds&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@import&lt;/code&gt; any CSS files required by your JS libraries (like date pickers or charts) in your CSS files instead of JS files&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Make a list of those built files in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/config/manifest.js&lt;/code&gt; so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sprockets-4&lt;/code&gt; will let you reference them with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;javascript_include_tag&lt;/code&gt; etc…&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;Remove webpack&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;the-solution&quot;&gt;The Solution&lt;/h2&gt;

&lt;h3 id=&quot;1-cssbundling-rails&quot;&gt;1. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cssbundling-rails&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;This gives you a nice &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/build-css&lt;/code&gt; command. It’s a bash script that compiles all your CSS/SCSS using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node-sass&lt;/code&gt;, which you will install in your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can configure multiple CSS entrypoints in here if you need. Mine looks something like this:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;./node_modules/sass/sass.js &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  ./app/assets/stylesheets/application.sass.scss:./app/assets/builds/application.css &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  ./app/assets/stylesheets/admin/application.sass.scss:./app/assets/builds/admin/application.css &lt;span class=&quot;nt&quot;&gt;--no-source-map&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--load-path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;node_modules &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--load-path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;vendor/assets/stylesheets &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;$@&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The outputs will all be sent to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/assets/builds&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Some of the JS libraries we use come with CSS dependencies. Previously I was &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import&lt;/code&gt;-ing them in JS files (which always felt weird in a Rails app). Now I can import them into my SCSS files like this:&lt;/p&gt;

&lt;div class=&quot;language-scss highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;../../../../node_modules/react-date-range/dist/styles&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;../../../../node_modules/@melloware/coloris/dist/coloris&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;../../../../vendor/assets/stylesheets/medium-editor&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Note: be sure to remove the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.css&lt;/code&gt; extension when importing CSS from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt;. That’s the &lt;a href=&quot;https://github.com/rails/cssbundling-rails/issues/92#issuecomment-1188440707&quot;&gt;only way the will import properly&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;2-jsbundling-rails-with-esbuild&quot;&gt;2. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsbundling-rails&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;esbuild&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;This will auto-create an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;esbuild.config.js&lt;/code&gt; where you can set your JS entrypoints. Mine looks something like this:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;esbuild&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;entryPoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;app/javascript/dashboard.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;app/javascript/frontend.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;app/javascript/onboarding.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;bundle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;sourcemap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;watch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;argv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;--watch&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;outdir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;app/assets/builds&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Like with CSS your outputs will all be sent to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/assets/builds&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;3-sprockets-4&quot;&gt;3. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sprockets 4&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;I’d avoided sprocket upgrades for years. Anytime I tried it just got messy and &lt;a href=&quot;https://github.com/rails/sprockets/blob/5b040f3bec503ded8a98c0c911c7a77dd1f5bf1b/UPGRADING.md?plain=1#L32&quot;&gt;their warnings in the upgrade guide&lt;/a&gt; definitely didn’t inspire confidence.&lt;/p&gt;

&lt;p&gt;In the end sprockets-4 was simple to get running. I created the sprockets &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/config/manifest.js&lt;/code&gt; file like this:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;//= link_tree ../builds&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//= link_tree ../images&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//= link_tree ../fonts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The first line references all those files we built in the previous steps. The next two let me use my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;image_url&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;font_url&lt;/code&gt; helpers as usual.&lt;/p&gt;

&lt;p&gt;The sprockets manifest replaces any &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Rails.application.config.assets.precompile&lt;/code&gt; you had before. Get rid of that entirely.&lt;/p&gt;

&lt;h3 id=&quot;4-global-jquery&quot;&gt;4. Global jQuery&lt;/h3&gt;

&lt;p&gt;Old Bootstrap, jQuery UI and some old plugins require a global jQuery object. At first I put this at the top of each of my entrypoints:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;jquery&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;jquery&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;jQuery&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;jquery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;…but that was not enough. I needed to move that to a separate file and then import that file. So like this:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// in app/javascript/jquery.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;jquery&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;jquery&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;jQuery&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;jquery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// in your other JS files&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;./jquery&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;jquery-ui&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;jquery-ujs&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;bootstrap-sass&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;5-bonus-tips&quot;&gt;5. Bonus Tips&lt;/h3&gt;

&lt;p&gt;Ensure your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/assets/builds&lt;/code&gt; exists by adding a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.keep&lt;/code&gt; file or similar. &lt;a href=&quot;https://stackoverflow.com/a/71142132&quot;&gt;Sprockets won’t work in production without it.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;esbuild&lt;/code&gt; doesn’t minify by default. Here’s how you can &lt;a href=&quot;https://github.com/rails/jsbundling-rails/issues/8#issuecomment-962138249&quot;&gt;conditionally minify your JS in production&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When importing your local JS files, make sure you import them without extensions so Sprockets can do its thing. So instead of&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;my/forms.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;remove the extension…&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;my/forms&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;</content><author><name></name></author><category term="ruby" /><category term="rails" /><category term="dev" /><summary type="html">After another confusing migration of a Rails app’s asset stack into I finally found a stack that I’m happy with for Rails 7.x.</summary></entry><entry><title type="html">Talking Sorbet on Ruby Rogues</title><link href="/notes/2021/09/15/ruby-rogues-sorbet-podcast.html" rel="alternate" type="text/html" title="Talking Sorbet on Ruby Rogues" /><published>2021-09-15T10:00:00-07:00</published><updated>2021-09-15T10:00:00-07:00</updated><id>/notes/2021/09/15/ruby-rogues-sorbet-podcast</id><content type="html" xml:base="/notes/2021/09/15/ruby-rogues-sorbet-podcast.html">&lt;p&gt;In August, 2021 I got to check off a Rubyist bucket list item and appear as a guest on the Ruby Rogues podcast.&lt;/p&gt;

&lt;p&gt;I was talking about Sorbet and my experiences bringing it into our Rails app.  The whole recording process was fun and enjoyable.  And after listening for &lt;strong&gt;years&lt;/strong&gt; I’m pleased to be part of that podcast’s illustrious back catalogue.&lt;/p&gt;

&lt;p&gt;Take a listen: &lt;a href=&quot;https://devchat.tv/ruby-rogues/using-typing-systems-in-ruby-with-sorbet-ft-alex-dunae-ruby-512/&quot;&gt;« Ruby Rogues episode 512 »&lt;/a&gt;&lt;/p&gt;</content><author><name></name></author><category term="sorbet" /><category term="ruby" /><category term="rails" /><category term="podcasts" /><category term="dev" /><summary type="html">In August, 2021 I got to check off a Rubyist bucket list item and appear as a guest on the Ruby Rogues podcast.</summary></entry><entry><title type="html">BC Vaccine Passport Data Format</title><link href="/notes/2021/09/08/bc-vaccine-passport-format.html" rel="alternate" type="text/html" title="BC Vaccine Passport Data Format" /><published>2021-09-08T00:00:00-07:00</published><updated>2021-09-08T00:00:00-07:00</updated><id>/notes/2021/09/08/bc-vaccine-passport-format</id><content type="html" xml:base="/notes/2021/09/08/bc-vaccine-passport-format.html">&lt;p&gt;BC’s COVID-19 vaccine passport is being distributed primarily via QR code.  I love QR codes and wanted to see what was encoded in there.&lt;/p&gt;

&lt;p&gt;Regardless of the ethics of everything that is going on, from a technical point of view I was pleased to see that the data are narrowly scoped and that the whole system works without provided data back to a central authority.&lt;/p&gt;

&lt;h3 id=&quot;bc-covid-19-vaccine-passport-data-format&quot;&gt;BC COVID-19 Vaccine Passport Data Format&lt;/h3&gt;

&lt;p&gt;The QR code payload is encoded as a JSON web token following the &lt;a href=&quot;https://spec.smarthealth.cards/&quot;&gt;SMART Health Card open spec&lt;/a&gt;.  When you scan it you get a string starting with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shc:/&lt;/code&gt;.  (This is &lt;a href=&quot;https://mikkel.ca/blog/digging-into-quebecs-proof-of-vaccination/&quot;&gt;the same format used in Quebec.&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;There is a handy &lt;a href=&quot;https://github.com/obrassard/shc-extractor&quot;&gt;JS project called obrassard/shc-extractor&lt;/a&gt; where you can decode that string and inspect it yourself.  The &lt;a href=&quot;https://github.com/obrassard/shc-extractor/blob/425910180bae13c60a38581a9f52e7bc5c2d3bb4/src/parsers.js#L23-L40&quot;&gt;code to decode the payload&lt;/a&gt; is pretty straightforward.&lt;/p&gt;

&lt;p&gt;The payload is signed and can be decoded using the public keys provided by the passport issuer.  In the case of BC it’s the Provincial Health Services Authority.  You can get the keys from &lt;a href=&quot;https://smarthealthcard.phsa.ca/v1/issuer/.well-known/jwks.json&quot;&gt;https://smarthealthcard.phsa.ca/v1/issuer/.well-known/jwks.json&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The payload contains the person’s&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;first name&lt;/li&gt;
  &lt;li&gt;last name&lt;/li&gt;
  &lt;li&gt;birth date&lt;/li&gt;
  &lt;li&gt;date, location and type of COVID-19 vaccine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There isn’t any other data included in the payload.&lt;/p&gt;

&lt;p&gt;Caching the public keys means you can verify vax passport QR codes entirely offline.&lt;/p&gt;

&lt;h3 id=&quot;vaccine-codes&quot;&gt;Vaccine codes&lt;/h3&gt;

&lt;p&gt;Each dose of administered vaccine has a specific code.  You can inspect the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http://snomed.info/sct&lt;/code&gt; coding key to see which type of vaccines were administered (&lt;a href=&quot;http://www.newswire.ca/en/releases/archive/December2020/15/c6404.html&quot;&gt;source&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;28581000087106&lt;/code&gt; – PFIZER-BIONTECH COVID-19 messenger ribonucleic acid 30 micrograms per 0.3 milliliter suspension for dilution for injection Pfizer Canada ULC-BioNTech Manufacturing GmbH (real clinical drug)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;28571000087109&lt;/code&gt; – MODERNA COVID-19 messenger ribonucleic acid-1273 100 micrograms per 0.5 milliliter liquid for injection Moderna Therapeutics Inc. (real clinical drug)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;28531000087107&lt;/code&gt; – Vaccine product against disease caused by Severe acute respiratory syndrome coronavirus 2 (medicinal product)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1119349007&lt;/code&gt; – Vaccine product containing only Severe acute respiratory syndrome coronavirus 2 messenger ribonucleic acid (medicinal product)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;sample-bc-vaccine-passport-contents&quot;&gt;Sample BC Vaccine Passport Contents&lt;/h3&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;header&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;alg&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ES256&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;zip&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;DEF&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;kid&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;XCqxdhhS7SWlPqihaUXovM_FjU65WeoBFGc_ppent0Q&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;payload&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;iss&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://smarthealthcard.phsa.ca/v1/issuer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;nbf&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1630885634&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;vc&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://smarthealth.cards#covid19&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://smarthealth.cards#immunization&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://smarthealth.cards#health-card&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;credentialSubject&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;fhirVersion&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;4.0.1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;fhirBundle&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;resourceType&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Bundle&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;collection&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;entry&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;fullUrl&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;resource:0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;resource&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;resourceType&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Patient&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;family&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;LASTNAME&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;given&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                            &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;FIRSTNAME&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;birthDate&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;1980-01-01&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;fullUrl&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;resource:1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;resource&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;resourceType&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Immunization&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;completed&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;vaccineCode&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;coding&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://hl7.org/fhir/sid/cvx&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;208&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://snomed.info/sct&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;28581000087106&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;patient&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;reference&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;resource:0&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;occurrenceDateTime&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2021-05-21&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;lotNumber&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;EW0199&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;performer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;actor&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;display&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Drop-in Vaccine Clinic&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;fullUrl&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;resource:2&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;resource&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;resourceType&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Immunization&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;completed&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;vaccineCode&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;coding&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://hl7.org/fhir/sid/cvx&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;208&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://snomed.info/sct&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;28581000087106&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;patient&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;reference&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;resource:0&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;occurrenceDateTime&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2021-07-25&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;lotNumber&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;FD7206&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;performer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;actor&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;display&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;UBC Vax Van&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;verifications&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;trustable&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;verifiedBy&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;XCqxdhhS7SWlPqihaUXovM_FjU65WeoBFGc_ppent0Q&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;origin&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://smarthealthcard.phsa.ca/v1/issuer&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;You can also &lt;a href=&quot;https://gist.github.com/alexdunae/49cc0ea95001da3360ad6896fa5677ec&quot;&gt;download the sample vaccine passport via a gist&lt;/a&gt;.&lt;/p&gt;

&lt;div class=&quot;feedback&quot;&gt;
  &lt;p&gt;
    I&apos;d love to hear any feedback you might have about this post.
    &lt;a href=&quot;https://twitter.com/mrmrbug&quot;&gt;Tweet me up at @mrmrbug&lt;/a&gt;
    or email me at &lt;a href=&quot;mailto:code@dunae.ca&quot;&gt;code@dunae.ca&lt;/a&gt;.
  &lt;/p&gt;

  &lt;p&gt;You can also &lt;a href=&quot;/notes/feed.xml&quot;&gt;grab a little bit of RSS&lt;/a&gt; or &lt;a href=&quot;/notes/&quot;&gt;check out the rest of the blog&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;</content><author><name></name></author><category term="json" /><category term="jwt" /><category term="covid-19" /><category term="qr-code" /><summary type="html">BC’s COVID-19 vaccine passport is being distributed primarily via QR code. I love QR codes and wanted to see what was encoded in there.</summary></entry><entry><title type="html">Using Database Triggers for Caching Counts</title><link href="/notes/2021/06/19/database-triggers-for-caching-counts.html" rel="alternate" type="text/html" title="Using Database Triggers for Caching Counts" /><published>2021-06-19T00:00:00-07:00</published><updated>2021-06-19T00:00:00-07:00</updated><id>/notes/2021/06/19/database-triggers-for-caching-counts</id><content type="html" xml:base="/notes/2021/06/19/database-triggers-for-caching-counts.html">&lt;blockquote&gt;
  &lt;p&gt;This is a story &lt;br /&gt;all about how&lt;br /&gt;
my counts got flipped&lt;br /&gt;turned rightside down&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Today’s post looks at ways to improve an application by leaning more heavily on the database to manage data.  Specifically we look at using database triggers for derived inventory counts, simplifying the application code and solving a deadlock along the way.&lt;/p&gt;

&lt;h2 id=&quot;moving-data-integrity-into-the-database&quot;&gt;Moving Data Integrity into the Database&lt;/h2&gt;

&lt;p&gt;Some of the most satisfying code cleanups I’ve done recently have involved reinforcing data integrity checks from the application down into the database.  Modern MySQL (and old Postgres) have plenty of options for add data constraints ranging from a simple &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NOT NULL&lt;/code&gt;  to &lt;a href=&quot;https://www.percona.com/blog/2020/10/02/how-to-use-check-constraint-in-mysql-8/&quot;&gt;more complex constraints&lt;/a&gt; and they are generally rock solid.&lt;/p&gt;

&lt;p&gt;Database-level constraints like these are useful because &lt;strong&gt;they run no matter what,&lt;/strong&gt; even if your application validation is bypassed.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;they run if you’re doing bulk &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INSERT INTO target (something) SELECT else FROM external_data&lt;/code&gt; from the command line&lt;/li&gt;
  &lt;li&gt;they run if you’re writing things like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE target SET something = (SELECT else)&lt;/code&gt;,&lt;/li&gt;
  &lt;li&gt;they run even if you’re &lt;em&gt;manually editing rows like a maniac&lt;/em&gt; 👺&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most times I’ll keep the application-level constraints as well.&lt;/p&gt;

&lt;p&gt;Here’s an example using &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/create-table-check-constraints.html&quot;&gt;MySQL 8 check constraints&lt;/a&gt; as well as application-level validation (in Rails in this case).  Both types of constraint help ensure &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;items.quantity_available&lt;/code&gt; never goes below 0:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;&lt;span class=&quot;gh&quot;&gt;diff --git a/app/models/item.rb b/app/models/item.rb
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+  validates :quantity_available, numericality: { only_integer: true, greater_than_or_equal_to: 0 }&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;&lt;span class=&quot;gh&quot;&gt;diff --git a/db/migrate/20210101000000_add_inventory_check_constraints.rb b/db/migrate/20210101000000_add_inventory_check_constraints.
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+class AddInventoryCheckConstraints &amp;lt; ActiveRecord::Migration[6.1]
+  def change
+    add_check_constraint :items, &quot;quantity_available &amp;gt;= 0&quot;, name: &quot;chk_items_minimum_quantity_available&quot;
+  end
+end
&lt;/span&gt;

&lt;span class=&quot;gh&quot;&gt;diff --git a/db/structure.sql b/db/structure.sql
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;CREATE TABLE `items` (
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+  CONSTRAINT `chk_items_minimum_quantity_available` CHECK (`quantity_available` &amp;gt;= 0)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;In this example we mirror the constraint (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;greater than or equal to zero&lt;/code&gt;) in both application code and in the database.  This redundancy gives us a few nice things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From the application-level constraint we get&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;nice error messages to display to the user (database constraint errors are usually pretty ugly)&lt;/li&gt;
  &lt;li&gt;easy-to-find validation rules for other developers when they’re browsing the application code (they don’t need to check the database schema)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;From the database-level constraint we get:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;a guarantee that any manual changes, data imports or database-level calculations will never create invalid data&lt;/li&gt;
  &lt;li&gt;rules that will be enforced if any other applications start working with our database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is all pretty straightforward. The rest of this post is going to look at using the database to actually create and mange valid data &lt;em&gt;within the database itself&lt;/em&gt;.&lt;/p&gt;

&lt;h2 id=&quot;update-constraints-set-volume--11&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE constraints SET volume = 11&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;The saddest entries I see in my bug tracker are: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction&lt;/code&gt;. They mean that there’s a &lt;a href=&quot;https://stackoverflow.com/a/2775003/559596&quot;&gt;database deadlock&lt;/a&gt; and I know we’re in for some tedious and delicate &lt;a href=&quot;https://en.wikipedia.org/wiki/Heisenbug&quot;&gt;Heisenbug&lt;/a&gt; hunting to trace down the exact set of conditions that caused the database to lock. Yuck.&lt;/p&gt;

&lt;p&gt;In one case case – issue #1330 from August 1, 2017 in the bug tracker - it became clear the deadlock was caused by our inventory updating code.&lt;/p&gt;

&lt;div class=&quot;full-width&quot;&gt;
&lt;img src=&quot;/notes/assets/deadlock-issue.png&quot; alt=&quot;Screenshot of a deadlock Github issue&quot; /&gt;
&lt;div class=&quot;image-caption&quot;&gt;Trigger warning...&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://tickit.ca&quot;&gt;The application&lt;/a&gt; generates tickets for live events, with a limited number of each ticket type available. In order to have a 100% accurate inventory count we queried the inventory immediately before adding any items to an order.&lt;/p&gt;

&lt;p&gt;This sort of code would run many times per second:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;k&quot;&gt;START&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TRANSACTION&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- get quantity immediately before adding items to an order to ensure 100% accuracy&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;SUM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quantity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line_items&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;item1&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- insert if available&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;INTO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line_items&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;order_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;quantity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;VALUES&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;order1&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;item1&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;COMMIT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;strong&gt;Here’s the problem:&lt;/strong&gt;  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SELECT SUM...&lt;/code&gt; would lock the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;line_items&lt;/code&gt; table at the same time as another purchaser was trying to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INSERT line_items&lt;/code&gt;, creating deadlocks.&lt;/p&gt;

&lt;p&gt;We needed a solution that would not run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SELECT SUM...&lt;/code&gt; in the middle of the critical path of our application.&lt;/p&gt;

&lt;h2 id=&quot;cache-it&quot;&gt;Cache it?&lt;/h2&gt;

&lt;p&gt;The first fix we tried to get rid of  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SELECT SUM...&lt;/code&gt; involved caching the inventory counts in Redis.  This had potential but our implementation was never solid (a story for another day). We eventually moved on to our current solution: &lt;strong&gt;cache the inventory counts directly in the database using triggers&lt;/strong&gt;.&lt;/p&gt;

&lt;h2 id=&quot;trigger-happy&quot;&gt;Trigger happy&lt;/h2&gt;

&lt;p&gt;A database trigger is…&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;…a special stored procedure that is run when specific actions occur within a database. Most triggers are defined to run when changes are made to a table’s data &lt;small&gt;&lt;a href=&quot;https://www.codeproject.com/Articles/5164724/What-is-a-Database-Trigger&quot;&gt;…&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can write triggers that automatically run after &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INSERT&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE&lt;/code&gt; actions.  Like constraints, they’re guaranteed to run no matter how you interact with your database (even &lt;em&gt;manual editing like a maniac&lt;/em&gt; 👺).  The trigger gets the value of the rows &lt;em&gt;before&lt;/em&gt; and &lt;em&gt;after&lt;/em&gt; your change, and you can use both old and new values to write some more SQL of your choosing.&lt;/p&gt;

&lt;p&gt;Importantly, if a trigger fails it rolls back the entire operation so we don’t need to worry about data getting out of sync.&lt;/p&gt;

&lt;h2 id=&quot;the-new-design&quot;&gt;The new design&lt;/h2&gt;
&lt;p&gt;The system we landed on to keep our inventory counts in sync worked like this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;listen for any change to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;line_items&lt;/code&gt; table&lt;/li&gt;
  &lt;li&gt;when a change is triggered, increment or decrement the related &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;items.quantity_sold&lt;/code&gt; field by the amount in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;line_items.quantity&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This meant no more &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SELECT SUM...&lt;/code&gt;. Just simple &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;step-add-an-inventory-count-column-to-items-table-and-model&quot;&gt;Step: Add an inventory count column to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;items&lt;/code&gt; table and model&lt;/h3&gt;

&lt;p&gt;This column is read-only to the application and will only be updated by the triggers.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;&lt;span class=&quot;gh&quot;&gt;diff --git a/app/models/item.rb b/app/models/item.rb
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;class Item &amp;lt; ApplicationRecord
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+  attr_readonly :quantity_sold
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;&lt;span class=&quot;gh&quot;&gt;diff --git a/db/migrate/20210101000000_add_quantity_sold.rb b/db/migrate/20210101000000_add_quantity_sold.
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+class AddQuantitySold &amp;lt; ActiveRecord::Migration[6.1]
+  # add counter column to the Items table - this will only be edited by the trigger
+  def change
+    add_columns :items, :quantity_sold, :integer, default: 0, null: false
+  end
+end
&lt;/span&gt;
diff --git a/db/structure.sql b/db/structure.sql
&lt;span class=&quot;p&quot;&gt;CREATE TABLE `items` (
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+ `quantity_sold` int NOT NULL DEFAULT &apos;0&apos;,&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;step-create-database-triggers-for-each-operation&quot;&gt;Step: Create database triggers for each operation&lt;/h3&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;c1&quot;&gt;-- the &quot;AFTER INSERT&quot; trigger&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- simply increments the value of quantity_sold (or decrements if the NEW.quantity is negative)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- we always update timestamps, too, but you may not need to&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TRIGGER&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;`line_items_after_insert_row_tr`&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AFTER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;`line_items`&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;EACH&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ROW&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;BEGIN&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;UPDATE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;quantity_sold&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quantity_sold&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;COALESCE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;NEW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quantity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;updated_at&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UTC_TIMESTAMP&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NEW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;END&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;c1&quot;&gt;--- the &quot;BEFORE DELETE&quot; trigger&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TRIGGER&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;`line_items_before_delete_row_tr`&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BEFORE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;`line_items`&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;EACH&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ROW&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;BEGIN&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;UPDATE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;quantity_sold&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quantity_sold&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;OLD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quantity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;updated_at&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UTC_TIMESTAMP&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;OLD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;END&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;c1&quot;&gt;--- the &quot;BEFORE UPDATE&quot; trigger&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;--- this is a special guard to forbid changing a line item&apos;s ITEM_ID once it has been set&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;--- adding that restriction dramatically simplified our trigger code and fits logically with&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;--- how our system works.  If you find your triggers getting crazy feel free to set your own guards&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;--- to limit the possible states your data can be in.&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;--- For MySQL the easiest way to signal an error is with SIGNAL SQLSTATE&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;--- https://dev.mysql.com/doc/refman/8.0/en/signal.html&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TRIGGER&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;`line_items_before_update_row_tr`&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BEFORE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;UPDATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;`line_items`&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;EACH&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ROW&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;BEGIN&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;IF&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NEW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;OLD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item_id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;OR&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;NEW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item_id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;OLD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item_id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;SIGNAL&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;SQLSTATE&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;45000&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;message_text&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Changes to LineItem#item_id are forbidden&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;END&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;IF&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;END&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;c1&quot;&gt;-- the &quot;AFTER UPDATE&quot; trigger&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- in order to reduce un-necessary update calls this trigger only runs&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- if an update actually changes the quantity&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- it also uses COALESCE to ensure our NEW.value is non-null&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TRIGGER&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;`line_items_after_update_row_tr`&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AFTER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;UPDATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;`line_items`&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;EACH&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ROW&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;BEGIN&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;IF&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NEW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quantity&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;OLD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quantity&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;delta_quantity_sold&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;COALESCE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;NEW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quantity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;COALESCE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;OLD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quantity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;UPDATE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;quantity_sold&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quantity_sold&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;COALESCE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;delta_quantity_sold&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;updated_at&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UTC_TIMESTAMP&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NEW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;END&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;IF&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;END&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;step-add-tests&quot;&gt;Step: Add tests&lt;/h3&gt;

&lt;p&gt;It’s important to be able to run your tests against the same version of database that you run in production.  That’s a good practice anyways, and thankfully Rails and modern CI systems make that super easy.&lt;/p&gt;

&lt;p&gt;Because this code was going to be largely out-of-sight, we added a decent number of unit tests to ensure every edge case results in proper counts.&lt;/p&gt;

&lt;h2 id=&quot;did-it-work-ya&quot;&gt;Did it work? Ya!&lt;/h2&gt;

&lt;p&gt;This system has been running without issue for several years now and is rock solid.  There are no more deadlocks since we no longer run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SELECT SUM...&lt;/code&gt; and so are putting way less presure on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;line_items&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;Moving such crucial code out of the application layer was nerve-wracking.  This post began by talking about how great it was to have constraints at both the application and database levels.  Well, &lt;strong&gt;this change moved the responsibility for data integrity entirely out of the application and into the database.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We knew the database would do a good job but we were worried about the developer experience.  Would the triggers be hard to work with?  Would they fail and be hard to debug?  Would the math be wrong and we wouldn’t know why? Would we forget how they worked?&lt;/p&gt;

&lt;p&gt;It also just seemed too simple.  We were used to always running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SUM&lt;/code&gt; on the entire data set.  This just did puny little &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-&lt;/code&gt; operations on a single row at a time, relying on the &lt;a href=&quot;https://en.wikipedia.org/wiki/Atomicity_(database_systems)&quot;&gt;atomicity&lt;/a&gt; of the database engine.&lt;/p&gt;

&lt;p&gt;Happily, our worries have not come true.  Triggers have been great.  &lt;strong&gt;I think the reason triggers worked so well is that we chose a stable, rarely-changing and narrowly-scoped slice of our application.&lt;/strong&gt;  I wouldn’t want triggers in a chunk of code that changed frequently.&lt;/p&gt;

&lt;h3 id=&quot;a-few-rails-specifics&quot;&gt;A few Rails specifics&lt;/h3&gt;

&lt;p&gt;This strategy works for any tech stack.  We use Rails, which give us a few niceties. The main one is that Rails stores the database migrations directly beside application code, keeping things in sync.  We don’t have dedicated DBAs, so the same people write SQL and Ruby, allowing for super close coordination.&lt;/p&gt;

&lt;p&gt;The other is that Rails makes it easy to spin up and destroy test databases using the same database engine we use in production.  This is key for getting good test coverage of our triggers.&lt;/p&gt;

&lt;p&gt;We manually write the trigger modifying code in our Rails migrations.  We looked at using the &lt;a href=&quot;https://github.com/jenseng/hair_trigger&quot;&gt;hair_trigger gem&lt;/a&gt; but I don’t think it played well with MySQL way back then.  It might now.  In any case, this code changes so rarely it’s not a big deal.&lt;/p&gt;

&lt;h2 id=&quot;further-reading&quot;&gt;Further Reading&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://evilmartians.com/chronicles/pulling-the-trigger-how-to-update-counter-caches-in-you-rails-app-without-active-record-callbacks&quot;&gt;This blog post&lt;/a&gt; by Evil Martians provides some great info about using database triggers.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://bugs.mysql.com/bug.php?id=11472&quot;&gt;MySQL bug #11472 about foreign keys and triggers&lt;/a&gt; holds a dear place in many people’s hearts.  Opened in 2005 it still persists unfixed.  People stop by to wish it happy birthday now.&lt;/p&gt;

&lt;h2 id=&quot;the-end&quot;&gt;The End&lt;/h2&gt;

&lt;p&gt;Thanks to &lt;a href=&quot;https://twitter.com/coreymaass/&quot;&gt;Corey Maas&lt;/a&gt;, &lt;a href=&quot;https://www.linkedin.com/in/ntewinkel/&quot;&gt;Nico teWinkel&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/yosukehasumi&quot;&gt;Yosuke Hasumi&lt;/a&gt; for reading drafts of this post.&lt;/p&gt;

&lt;p&gt;And thank you for reading it!&lt;/p&gt;

&lt;div class=&quot;feedback&quot;&gt;
  &lt;p&gt;
    I&apos;d love to hear any feedback you might have about this post.
    &lt;a href=&quot;https://twitter.com/mrmrbug&quot;&gt;Tweet me up at @mrmrbug&lt;/a&gt;
    or email me at &lt;a href=&quot;mailto:code@dunae.ca&quot;&gt;code@dunae.ca&lt;/a&gt;.
  &lt;/p&gt;

  &lt;p&gt;You can also &lt;a href=&quot;/notes/feed.xml&quot;&gt;grab a little bit of RSS&lt;/a&gt; or &lt;a href=&quot;/notes/&quot;&gt;check out the rest of the blog&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;</content><author><name></name></author><category term="databases" /><category term="mysql" /><category term="ruby" /><category term="rails" /><category term="dev" /><category term="ecommerce" /><category term="inventory" /><summary type="html">This is a story all about how my counts got flippedturned rightside down</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/notes/assets/tigger-warning.jpg" /><media:content medium="image" url="/notes/assets/tigger-warning.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Sorbet Journey, Part 4: Sorbet Stability</title><link href="/notes/2021/05/25/sorbet-stability.html" rel="alternate" type="text/html" title="Sorbet Journey, Part 4: Sorbet Stability" /><published>2021-05-25T14:00:00-07:00</published><updated>2021-05-25T14:00:00-07:00</updated><id>/notes/2021/05/25/sorbet-stability</id><content type="html" xml:base="/notes/2021/05/25/sorbet-stability.html">&lt;section class=&quot;blog-toc&quot;&gt;
  &lt;h4&gt;Sorbet Journey&lt;/h4&gt;

  &lt;p&gt;
    This is an ongoing series about adding Sorbet to a mature Rails code base.
    &lt;a href=&quot;https://twitter.com/mrmrbug&quot;&gt;Follow me on Twitter&lt;/a&gt; to find out
    when updates are posted.
  &lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;
      &lt;a href=&quot;/notes/2020/12/01/sorbet-journey-types-motivation.html&quot;&gt;Part 1: Why Add Types to a Rails App&lt;/a&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;a href=&quot;/notes/2020/12/09/adding-sorbet-to-an-existing-gem.html&quot;&gt;Part 2: Adding Sorbet to an Existing Ruby Gem&lt;/a&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;a href=&quot;/notes/2020/12/28/a-typical-day-adding-sorbet-to-rails.html&quot;&gt;Part 3: A Typical Day with Adding Sorbet to a Rails App&lt;/a&gt;
    &lt;/li&gt;

    &lt;li&gt;
      &lt;a href=&quot;/notes/2021/05/25/sorbet-stability.html&quot;&gt;Part 4: Sorbet Stability&lt;/a&gt;
    &lt;/li&gt;

    &lt;li&gt;
      &lt;a href=&quot;/notes/2021/09/15/ruby-rogues-sorbet-podcast.html&quot;&gt;Part X: Ruby Rogues Podcast&lt;/a&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/section&gt;

&lt;aside class=&quot;disclaimer&quot;&gt;
    Head&apos;s up: this is a high level summary of my impressions of working with Sorbet. It assumes you&apos;ve already read the Sorbet docs and are interested in an experience report. It&apos;s not really a tutorial.
&lt;/aside&gt;

&lt;p&gt;It’s been six months since we started using Sorbet in earnest, so it seems like a fitting time for reflection.&lt;/p&gt;

&lt;h3 id=&quot;tldr&quot;&gt;TL;DR&lt;/h3&gt;
&lt;p&gt;Sorbet is great and I’m glad we added it.  There is an occassional bit of awkwardness, but the benefits remain well worth it. I much prefer Rails with Sorbet on top.&lt;/p&gt;

&lt;h3 id=&quot;where-are-we-at&quot;&gt;Where Are We At?&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;/notes/2020/12/01/sorbet-journey-types-motivation.html&quot;&gt;Our original goal&lt;/a&gt; was to be able to refactor and develop with confidence on our “mature” Rails codebase.  It had reached a size where it was hard to hold all the pieces in your head, and subsystems were stable enough that they wouldn’t need any dev time for months on end.&lt;/p&gt;

&lt;p&gt;We hoped that adding types would…&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;surface existing subtle bugs,&lt;/li&gt;
  &lt;li&gt;prevent collateral damage when refactoring, and&lt;/li&gt;
  &lt;li&gt;allow us to move thoughtfully and not break things.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We added types incrementally, usually whenever we started working in a new area of the codebase.&lt;/p&gt;

&lt;p&gt;We started with models, then moved on to service classes, internal libraries and background workers.  We typed a handful of view helpers (which were surprisingly buggy) and mailers.&lt;/p&gt;

&lt;p&gt;We still have not typed any controllers yet.&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Sorbet Strictness&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;% of files&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
  &lt;tr&gt;&lt;th&gt;ignore&lt;/th&gt;&lt;td align=&quot;right&quot;&gt;3%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;false&lt;/th&gt;&lt;td align=&quot;right&quot;&gt;35%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;true&lt;/th&gt;&lt;td align=&quot;right&quot;&gt;39%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;strict&lt;/th&gt;&lt;td align=&quot;right&quot;&gt;16%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;strong&lt;/th&gt;&lt;td align=&quot;right&quot;&gt;7%&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;thinking-in-types-bug-squashing&quot;&gt;Thinking in Types: Bug Squashing&lt;/h3&gt;

&lt;p&gt;Sorbet has changed the way we start to investigate bugs.  Now, we start by looking to see if the code involved is strictly typed yet.  If not, we’ll start by adding types to the code.  The act of adding types is a great warm up for getting familiar with a section of code.  It gives us a chance to reacquaint ourselves with our assumptions and the data types involved.&lt;/p&gt;

&lt;p&gt;The process of adding types will often reveal the bug, somewhere around half the time.&lt;/p&gt;

&lt;p&gt;If the bug isn’t revealed, learning that the types are ok lets us exclude a whole class of bugs from our bug hunt and sharpen our focus.&lt;/p&gt;

&lt;h3 id=&quot;thinking-in-types-nil&quot;&gt;Thinking in Types: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nil&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;A subtle but far-reaching change in my own thinking has been how &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nil&lt;/code&gt; now stands out as a wild and infectious value.  The explicitness that Sorbet requires with nils (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T.let&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T.must&lt;/code&gt;) has made me think very carefully about what should be nullable.  Since anything that is nullable now feels like it’s going to be more work (because it is), we avoid nullable values unless they actually make sense.&lt;/p&gt;

&lt;p&gt;This null-avoidance has even spread to the schema database, where we’ve tightened up some sloppily defined column types.&lt;/p&gt;

&lt;p&gt;There will always be plenty of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nil&lt;/code&gt;s in the code; we’re just more aware of them now.&lt;/p&gt;

&lt;p&gt;(I’ve also gotten pretty enamoured with the concept of &lt;a href=&quot;https://en.wikipedia.org/wiki/Option_type&quot;&gt;Maybe types&lt;/a&gt;, but that’s a story for a different day.)&lt;/p&gt;

&lt;h3 id=&quot;thinking-in-types-hashes--structs&quot;&gt;Thinking in Types: Hashes =&amp;gt; Structs&lt;/h3&gt;

&lt;p&gt;Passing around complex values like hashes is another area where Sorbet’s influence is felt.  Defining the shapes of hashes in Sorbet isn’t amazing, so we’ve converted a good number to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T::Struct&lt;/code&gt;s to take advantage of all the type goodness.&lt;/p&gt;

&lt;p&gt;So data structures like:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;payment: &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;application_fee: &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;amount_to_refund_cents: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;123_45&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;become the much more lovely…&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RefundResult&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Struct&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:payment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Payment&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:application_fee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Stripe&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ApplicationFee&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:amount_to_refund_cents&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Integer&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;We did some of this before Sorbet, but Sorbet just made it more compelling.&lt;/p&gt;

&lt;h3 id=&quot;thinking-in-types-arrays-vs-collectionproxy&quot;&gt;Thinking in Types: Arrays vs CollectionProxy&lt;/h3&gt;

&lt;p&gt;Before Sorbet, we’d treat arrays of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ActiveRecord&lt;/code&gt; objects and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ActiveRecord::Associations::CollectionProxy&lt;/code&gt; collections identically.  In some ways this was really nice; after all one of the joys of Ruby and Rails is duck-typing and just iterating without a care in the world.&lt;/p&gt;

&lt;p&gt;Sorbet makes us aware of the differences, and it’s sometimes being useful to be more explicit about these.  Methods like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#sum&lt;/code&gt; work pretty differently on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Enumerable&lt;/code&gt; compared to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CollectionProxy&lt;/code&gt; so its nice to know what we’re working with.&lt;/p&gt;

&lt;p&gt;This change isn’t a pure net positive since it adds a level of strictness we don’t always need.  I’d call this one more 50/50.&lt;/p&gt;

&lt;h3 id=&quot;tooling&quot;&gt;Tooling&lt;/h3&gt;

&lt;p&gt;The Sorbet tooling remains pretty good.  Or rather, we’ve stuck with the tooling back from when we started and haven’t needed to move on.&lt;/p&gt;

&lt;p&gt;Our stack is still just &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet-rails&lt;/code&gt;, a little bit of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet-typed&lt;/code&gt; and the Sublime LSP plugin.  We had always intended to &lt;a href=&quot;https://github.com/Shopify/tapioca/issues/114&quot;&gt;move to tapioca&lt;/a&gt; since it seemed like The Right Way to do things, but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet-rails&lt;/code&gt; has given us everything that we need.&lt;/p&gt;

&lt;p&gt;We don’t have any strict process for re-generating Sorbet’s hidden definitions.  We update gem dependencies all the time (via Dependabot) and only rarely need to make any further updates.  Sorbet continues to work just fine.&lt;/p&gt;

&lt;p&gt;A few updates to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet-runtime&lt;/code&gt; have caused issues, but they’ve all been resolved by just waiting for the next weekly release.&lt;/p&gt;

&lt;h2 id=&quot;onward&quot;&gt;Onward&lt;/h2&gt;

&lt;p&gt;We &amp;lt;3 Sorbet.  It’s great, and it’s made working on old code feel much more secure.&lt;/p&gt;

&lt;p&gt;The stock tooling has been enough for us to get plenty of benefits with minimal fuss and we’ll stick with it for the time being.&lt;/p&gt;

&lt;p&gt;I expected to have to get more into the guts of things (&lt;a href=&quot;https://github.com/sorbet/sorbet/pull/4161&quot;&gt;and did a little bit&lt;/a&gt;) but Sorbet has been pleasantly, wonderfully boring.&lt;/p&gt;

&lt;div class=&quot;feedback&quot;&gt;
  &lt;p&gt;
    I&apos;d love to hear any feedback you might have about this post.
    &lt;a href=&quot;https://twitter.com/mrmrbug&quot;&gt;Tweet me up at @mrmrbug&lt;/a&gt;
    or email me at &lt;a href=&quot;mailto:code@dunae.ca&quot;&gt;code@dunae.ca&lt;/a&gt;.
  &lt;/p&gt;

  &lt;p&gt;You can also &lt;a href=&quot;/notes/feed.xml&quot;&gt;grab a little bit of RSS&lt;/a&gt; or &lt;a href=&quot;/notes/&quot;&gt;check out the rest of the blog&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;</content><author><name></name></author><category term="sorbet" /><category term="ruby" /><category term="rails" /><category term="dev" /><summary type="html">Sorbet Journey This is an ongoing series about adding Sorbet to a mature Rails code base. Follow me on Twitter to find out when updates are posted. Part 1: Why Add Types to a Rails App Part 2: Adding Sorbet to an Existing Ruby Gem Part 3: A Typical Day with Adding Sorbet to a Rails App Part 4: Sorbet Stability Part X: Ruby Rogues Podcast</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/notes/assets/sorbet-logo-white-sparkles.png" /><media:content medium="image" url="/notes/assets/sorbet-logo-white-sparkles.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Workshop Notes: Intro to React and Typescript</title><link href="/notes/2021/03/10/introducing-react-and-typescript.html" rel="alternate" type="text/html" title="Workshop Notes: Intro to React and Typescript" /><published>2021-03-10T09:00:00-08:00</published><updated>2021-03-10T09:00:00-08:00</updated><id>/notes/2021/03/10/introducing-react-and-typescript</id><content type="html" xml:base="/notes/2021/03/10/introducing-react-and-typescript.html">&lt;p&gt;In February, 2021 I led a fun little workshop introducing React and Typescript via the &lt;a href=&quot;https://www.nic.bc.ca/programs/business-applied-studies/digital-design-development/&quot;&gt;Interactive Media and Design program at North Island College&lt;/a&gt;.  The target audience was pretty broad: students, faculty looking to do some continuing education and members of the community.&lt;/p&gt;

&lt;p&gt;I’ve always enjoyed presenting but haven’t done much lately essentially due to time restrictions.  I’m hoping to do more. These are some notes to myself for the future.&lt;/p&gt;

&lt;iframe src=&quot;https://player.vimeo.com/video/521603815&quot; width=&quot;640&quot; height=&quot;332&quot; frameborder=&quot;0&quot; allow=&quot;autoplay; fullscreen; picture-in-picture&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;small&gt;&lt;a href=&quot;https://vimeo.com/521603815&quot;&gt;Ingenuity on the Edge React with Alex Dunae - Compressed&lt;/a&gt; from &lt;a href=&quot;https://vimeo.com/sfad&quot;&gt;NIC School of Fine Art + Design&lt;/a&gt; on &lt;a href=&quot;https://vimeo.com&quot;&gt;Vimeo&lt;/a&gt;.&lt;/small&gt;&lt;/p&gt;

&lt;h3 id=&quot;notes-on-presenting&quot;&gt;Notes on Presenting&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Regular commits to source control during the presentation worked great, so people did not need to stay up to date with live coding.&lt;/li&gt;
  &lt;li&gt;Live-ish coding worked suprisingly well.  I had the entire app written in steps and could cut and paste large chunks from my second monitor, but typing and talk was the most natural since it showed the true development flow.  This was especially true when talking about Typescript and autocomplete.&lt;/li&gt;
  &lt;li&gt;Having &lt;a href=&quot;/downloads/2021-nic-react-workshop.pdf&quot;&gt;intro slides&lt;/a&gt; was vital. I had planned to talk for about 20 minutes in the beginning about high-level concepts but didn’t plan on having slides until a few days before.  I quickly made some up and they were really beneficial.  Part of that benefit was that creating the slides was an additional editing step, forcing me to be even more concise and organized.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://carbon.now.sh/&quot;&gt;carbon.now.sh&lt;/a&gt; is amazing for creating minimal code screenshots&lt;/li&gt;
  &lt;li&gt;Talking slowly was well received.  I left huge gaps at the end of “paragraphs”, spoke slowly and repeated my concluding sentences. I thought that this would be annoying, but it seemed to work well with the diverse audience.&lt;/li&gt;
  &lt;li&gt;I left a few areas that could be cut out entirely (e.g. adding Sparklines) if time got tight. It was helpful to have escape hatches since we ended up running long.&lt;/li&gt;
  &lt;li&gt;The college recorded the workshop and managed Q&amp;amp;A, which let me just focus on the workshop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workshop was virtual due to Covid, and I was pretty concerned about not getting any visual feedback.  In the end it was fine, but next time I would like to find a way to get a little bit more interaction with the attendees.&lt;/p&gt;

&lt;h3 id=&quot;source-control-as-handout-material&quot;&gt;Source Control as Handout Material&lt;/h3&gt;

&lt;p&gt;I created a &lt;a href=&quot;https://github.com/alexdunae/nic-react-base&quot;&gt;bare source repository&lt;/a&gt; that participants could clone as their starting point and posted the &lt;a href=&quot;https://nic-react.netlify.app&quot;&gt;live project on Netlify&lt;/a&gt;.  While I live coded I pushed all the changes to their &lt;a href=&quot;https://github.com/alexdunae/nic-react-base/commits/deploy&quot;&gt;own branch&lt;/a&gt;, keeping the original bare repo clean.&lt;/p&gt;

&lt;h3 id=&quot;links&quot;&gt;Links&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/alexdunae/nic-react-base&quot;&gt;Blank starting repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/alexdunae/nic-react-base/commits/deploy&quot;&gt;Commits during the workshop&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/downloads/2021-nic-react-workshop.pdf&quot;&gt;Keynote slides&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://player.vimeo.com/video/521603815&quot;&gt;Video recording of the workshop&lt;/a&gt;&lt;/p&gt;</content><author><name></name></author><category term="js" /><category term="react" /><category term="typescript" /><category term="workshops" /><category term="dev" /><summary type="html">In February, 2021 I led a fun little workshop introducing React and Typescript via the Interactive Media and Design program at North Island College. The target audience was pretty broad: students, faculty looking to do some continuing education and members of the community.</summary></entry><entry><title type="html">Sorbet Journey, Part 3: A Typical Day Adding Sorbet to a Rails App</title><link href="/notes/2020/12/28/a-typical-day-adding-sorbet-to-rails.html" rel="alternate" type="text/html" title="Sorbet Journey, Part 3: A Typical Day Adding Sorbet to a Rails App" /><published>2020-12-28T14:27:05-08:00</published><updated>2020-12-28T14:27:05-08:00</updated><id>/notes/2020/12/28/a-typical-day-adding-sorbet-to-rails</id><content type="html" xml:base="/notes/2020/12/28/a-typical-day-adding-sorbet-to-rails.html">&lt;section class=&quot;blog-toc&quot;&gt;
  &lt;h4&gt;Sorbet Journey&lt;/h4&gt;

  &lt;p&gt;
    This is an ongoing series about adding Sorbet to a mature Rails code base.
    &lt;a href=&quot;https://twitter.com/mrmrbug&quot;&gt;Follow me on Twitter&lt;/a&gt; to find out
    when updates are posted.
  &lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;
      &lt;a href=&quot;/notes/2020/12/01/sorbet-journey-types-motivation.html&quot;&gt;Part 1: Why Add Types to a Rails App&lt;/a&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;a href=&quot;/notes/2020/12/09/adding-sorbet-to-an-existing-gem.html&quot;&gt;Part 2: Adding Sorbet to an Existing Ruby Gem&lt;/a&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;a href=&quot;/notes/2020/12/28/a-typical-day-adding-sorbet-to-rails.html&quot;&gt;Part 3: A Typical Day with Adding Sorbet to a Rails App&lt;/a&gt;
    &lt;/li&gt;

    &lt;li&gt;
      &lt;a href=&quot;/notes/2021/05/25/sorbet-stability.html&quot;&gt;Part 4: Sorbet Stability&lt;/a&gt;
    &lt;/li&gt;

    &lt;li&gt;
      &lt;a href=&quot;/notes/2021/09/15/ruby-rogues-sorbet-podcast.html&quot;&gt;Part X: Ruby Rogues Podcast&lt;/a&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/section&gt;

&lt;aside class=&quot;disclaimer&quot;&gt;
    Head&apos;s up: this is a high level summary of my impressions of working with Sorbet. It assumes you&apos;ve already read the Sorbet docs and are interested in an experience report. It&apos;s not really a tutorial.
&lt;/aside&gt;

&lt;h3 id=&quot;in-this-episode-of-sorbet-journey&quot;&gt;In This Episode of Sorbet Journey…&lt;/h3&gt;

&lt;p&gt;We’re a few weeks in working with Sorbet in our Rails mainline app and we’ve found there’s a natural kind of rhythm to the process.  The task at hand is a refactor of the way a purchase’s line items are handled.  This requires some big, wide-reaching changes so we’re taking some dev cycles to do some deep cleaning and tech-debt paydown.&lt;/p&gt;

&lt;p&gt;(These big changes are what made Sorbet so appealing – it’s giving us more confidence around making big, wide-reaching changes.)&lt;/p&gt;

&lt;h3 id=&quot;but-first-make-sure-you-have-tests&quot;&gt;But First: Make Sure You Have Tests&lt;/h3&gt;

&lt;p&gt;Having a comprehensive test suite has been super important as part of adding Sorbet to our Rails application.  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T.must&lt;/code&gt; and friends raise exceptions on invalid data.&lt;/p&gt;

&lt;p&gt;Many times we’ve gotten everything passing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;srb tc&lt;/code&gt; typechecker beautifully only to have CI explode with hundreds of failures because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T.must&lt;/code&gt; got a nil or some typed method got passed something from an untyped part of the code base.&lt;/p&gt;

&lt;p&gt;This is a good thing, since Sorbet is catching undefined behaviour and forcing explicit code. But without comprehensive tests we would be having a bad time.&lt;/p&gt;

&lt;h2 id=&quot;the-usual-steps&quot;&gt;The Usual Steps&lt;/h2&gt;

&lt;p&gt;So, these are the steps happen pretty much every time we start adding Sorbet to a part of the app.&lt;/p&gt;

&lt;h3 id=&quot;step-increase-strictness-check-for-breakage&quot;&gt;Step: Increase Strictness, Check for Breakage&lt;/h3&gt;

&lt;p&gt;We chose an area, set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;typed: true&lt;/code&gt; if it wasn’t already and then add one or two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sig {}&lt;/code&gt; declarations in key locations. We’ll often start by typing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attr_readers&lt;/code&gt; and initializers since they affect most of the file.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;&lt;span class=&quot;p&quot;&gt;class ParamsParser
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+  extend T::Sig
+
+  sig { returns(T.any(LineItem::RelationType, T::Array[LineItem])) }
&lt;/span&gt;   attr_reader :line_items
&lt;span class=&quot;gi&quot;&gt;+
+  sig { params(line_items: T.any(LineItem::RelationType, T::Array[LineItem])).void }
&lt;/span&gt;   def initialize(line_items)
     @line_items = line_items
   end
&lt;span class=&quot;p&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;step-run-sorbet-rails-on-affected-models&quot;&gt;Step: Run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet-rails&lt;/code&gt; on Affected Models&lt;/h3&gt;

&lt;p&gt;We’re using &lt;a href=&quot;https://github.com/chanzuckerberg/sorbet-rails&quot;&gt;sorbet-rails&lt;/a&gt; to get Rails and Sorbet playing nicely together.  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet-rails&lt;/code&gt; lets you generate RBI files for models, controllers and other Rails files.  In order to make our migration to Sorbet as low-impact as possible, we didn’t run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet-rails&lt;/code&gt; on many files to start.  Just enough to get a feel for things.&lt;/p&gt;

&lt;p&gt;Because Sorbet needs to know &lt;em&gt;something&lt;/em&gt; about those files, it created basic stubs for all our models in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hidden-definitions&lt;/code&gt; RBI file.  The auto-generated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hidden-definitions&lt;/code&gt; are pretty good, but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet-rails&lt;/code&gt; has Rails-specific knowledge that make enums and relations work properly.&lt;/p&gt;

&lt;p&gt;So, we’ll run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet-rails&lt;/code&gt; on specific models as we come across them as part of our daily work. This will generate a proper RBI file (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet/rails-rbi/models/your_model.rbi&lt;/code&gt;) for each model with enum types, relation types and knowledge of some of our model mixins like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;money-rails&lt;/code&gt; (more on that in a future post).&lt;/p&gt;

&lt;h3 id=&quot;step-rewrite-callbacks&quot;&gt;Step: Rewrite Callbacks&lt;/h3&gt;

&lt;p&gt;Our single most frequent change is with ActiveRecord callbacks.  Sorbet doesn’t like Rails callbacks using procs (&lt;a href=&quot;https://github.com/chanzuckerberg/sorbet-rails#after_commit-and-other-callbacks&quot;&gt;you can’t use instance methods in proc callbacks&lt;/a&gt;) so we rewrite them with actual methods.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;&lt;span class=&quot;p&quot;&gt;class RedemptionCode &amp;lt; ApplicationRecord
&lt;/span&gt;&lt;span class=&quot;gd&quot;&gt;-  before_save do
-    self.code = code.to_s.downcase
-  end
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+  before_save :normalize_before_save
+
+  private
+
+  def normalize_before_save
+    self.code = code.to_s.downcase
+  end
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Easy enough.&lt;/p&gt;

&lt;h3 id=&quot;step-add-nil-guards&quot;&gt;Step: Add Nil Guards&lt;/h3&gt;

&lt;p&gt;When we first tried Sorbet about a year ago our code ended up littered with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T.must&lt;/code&gt; everywhere (you wrap &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T.must&lt;/code&gt; around code to promise Sorbet that it is not null).  This time around I spent a bit of time learning about what Sorbet can know about code, specifically about &lt;a href=&quot;https://sorbet.org/docs/flow-sensitive#limitations-of-flow-sensitivity&quot;&gt;what might be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nil&lt;/code&gt;&lt;/a&gt;.  Funny how learning the rules makes things easier…&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;c1&quot;&gt;# Here&apos;s an example...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MyModel&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;ticket_count&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# we know this will _probably_ return the same value each time, but Sorbet doesn&apos;t&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;get_from_database&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ticket_count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;record&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;MyModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# This won&apos;t work with Sorbet nil checking since&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Sorbet calls `record.ticket_count` two times and&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# doesn&apos;t do a nil check the second time&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;record&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ticket_count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;record&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ticket_count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Assigning a variable and doing a T.must assertion is the&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# way to Sorbet happiness&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ticket_count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;must&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;record&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ticket_count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ticket_count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Some code like this is still inevitable:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;&lt;span class=&quot;p&quot;&gt;def not_started?
&lt;/span&gt;&lt;span class=&quot;gd&quot;&gt;-    starts_at? &amp;amp;&amp;amp; starts_at &amp;gt; Time.now
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+    starts_at? &amp;amp;&amp;amp; T.must(starts_at) &amp;gt; Time.now
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;But for many cases a well-placed variable assignment can limit the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T.must&lt;/code&gt; clutter.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;&lt;span class=&quot;gd&quot;&gt;-  result = quantity_per_item[item_id]&amp;amp;.seat_ids.take(quantity)
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+
+  seat_ids = T.must(quantity_per_item[item_id].seat_ids) if quantity_per_item[item_id]&amp;amp;.seat_ids
+  result = item.seat_ids.take(quantity)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h2 id=&quot;interesting-code-changes&quot;&gt;Interesting Code Changes&lt;/h2&gt;

&lt;h3 id=&quot;refactoring-hashes-to-a-proper-data-structure&quot;&gt;Refactoring Hashes to a Proper Data Structure&lt;/h3&gt;

&lt;p&gt;One of the core refactors we have been tackling is taking a Hash of user input (modifying the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Item&lt;/code&gt;s in an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Order&lt;/code&gt;) and turning it into a strictly defined data structure.  We could have done this without Sorbet, but having Sorbet typecheck during development and throw errors as part of our test runs made this refactor much easier.&lt;/p&gt;

&lt;p&gt;The data structure we ended up with was like this:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;c1&quot;&gt;# typed: strict&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# A container for parsed and sanitized &quot;modify cart&quot; requests.&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Usually created via ParseCartQuantityParams.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CartItemParams&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Struct&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;extend&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Sig&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;prop&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:quantity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Integer&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;prop&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:seat_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Integer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;default: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# various other props and methods&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Which were then created like this:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;&lt;span class=&quot;gi&quot;&gt;+ sig { params(item_id: Integer, param: T::Hash[Symbol, T.untyped]).returns(CartItemParams) }
&lt;/span&gt;  def parse_quantity_param(item_id, param)
&lt;span class=&quot;gi&quot;&gt;+    # all the parsing and sanitizing rules for CartItemParams in one place
&lt;/span&gt;  end&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;And in dozens of files, we can now made changes like this:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;&lt;span class=&quot;p&quot;&gt;module Seats
&lt;/span&gt;  class UpdateForOrderByQuantity
&lt;span class=&quot;gi&quot;&gt;+   sig { params(item_quantity: CartItemParams).void }
&lt;/span&gt;    def update(item_quantity)
&lt;span class=&quot;gd&quot;&gt;-      # lots of unconfident code like...
-      quantity = item_quantity.fetch(:quantity, nil)&amp;amp;.to_i
-      seat_ids = Array(item_quantity.fetch(:seat_ids, []]).compact.map(&amp;amp;:to_i).uniq
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+
+      # and instead we can have our nice object
+      item_quantities.quantity
+      item_quantities.seat_ids.empty?
&lt;/span&gt;  end
&lt;span class=&quot;p&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;em&gt;Just to re-iterate, all of the above is possible without Sorbet.  Sorbet just made it easier and gave us much more confidence that it actually worked.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;view-helpers-yuck&quot;&gt;View Helpers (Yuck)&lt;/h3&gt;

&lt;p&gt;Rails’ view helpers have always been a bit of a nasty part of our code base.  That continues.  At least now they’re nasty with types.&lt;/p&gt;

&lt;p&gt;The problem is that our view helpers are unstructured.  Our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ApplicationHelper&lt;/code&gt; has code that deals with routing, timezones, currency formatting, date formatting and meta tags.  I’m not sure what’s in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BaseHelper&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CoreHelper&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StoreHelper&lt;/code&gt;.  Maybe spiders?&lt;/p&gt;

&lt;p&gt;Many of our helpers also call global controller helper methods, like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_user&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_store&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Adding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;typed: true&lt;/code&gt; to the top of most view helpers caused dozens of errors that we weren’t ready to tackle yet.  Instead we did a bit of reorganizing.  We moved the methods we were ready to add types to new, narrowly defined helper modules left the other ones as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;typed: false&lt;/code&gt; for now.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;&lt;span class=&quot;gd&quot;&gt;--- /dev/null
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+++ b/app/helpers/currency_helper.rb
+module CurrencyHelper
+   sig { params(amount: T.nilable(Money), precision: Integer).returns(String) }
+   def currency(amount, precision: 2)
+     amount ||= Money.zero
+     number_to_currency(amount.round, precision: precision) || &apos;&apos;
+   end
+end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Unfortunately we currently use some of these methods in non-view parts of code.  For example, that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;currency&lt;/code&gt; formatting method is called in some models’ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to_json&lt;/code&gt; methods.  So for now we have code like this:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;&lt;span class=&quot;p&quot;&gt;class SomeModel
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+  include CurrencyHelper
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet-rails&lt;/code&gt; creates a module with all of your helpers in it but we weren’t able to get it working.  We’ll dig deeper when we start working on more code that uses helpers.&lt;/p&gt;

&lt;p&gt;This whole area is a work in progress. It feels like Sorbet just surfaced a bunch of unpleasant code that already existed, so we don’t blame Sorbet for all this, but the best way forward isn’t immediately clear.&lt;/p&gt;

&lt;h3 id=&quot;converting-request-params-to-hashes&quot;&gt;Converting Request Params to Hashes&lt;/h3&gt;

&lt;p&gt;Right near the end of this refactor we finally touched the boundary where some user input comes in: a JSON API endpoint.  Our internal code had all been operating happily on a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Hash&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Symbol&lt;/code&gt; keys (i.e. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T::Hash[Symbol, T.untyped]&lt;/code&gt;).  but our API endpoint had &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ActionController::Parameters&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My first instinct was to update our signatures to accept parameters:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;&lt;span class=&quot;gd&quot;&gt;- sig { params(params: T::Hash[Symbol, T.untyped]) }
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+ sig { params(params: T.any(T::Hash[Symbol, T.untyped], ActionController::Parameters)) }&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;But then I remembered &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HashWithIndifferentAccess&lt;/code&gt; and hashes with string keys and remebered all the subtle issues that come up with dealing hash types (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to_unsafe_h&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deep_symbolize_keys&lt;/code&gt;, etc…).&lt;/p&gt;

&lt;p&gt;So instead we took the opportunity to lock down the hashes we accept.  We left the Sorbet signature unchanged and turned &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ActionController::Parameters&lt;/code&gt; into a hash earlier:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;&lt;span class=&quot;gd&quot;&gt;- purchase_params = ActionController::Parameters.new(params).permit(*PURCHASE_PARAM_KEYS)
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+ purchase_params = ActionController::Parameters.new(params).permit(*PURCHASE_PARAM_KEYS).to_h&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;We haven’t added Sorbet to any controllers yet, so this may change.  And we may change the place where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to_h&lt;/code&gt; gets called.  But at least we have this defined somewhere for once.  Eliminating vague behaviour is exactly what we wanted to get from Sorbet.&lt;/p&gt;

&lt;h3 id=&quot;relations-arrays-and-relationtype&quot;&gt;Relations, Arrays and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RelationType&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;Throughout our app we had many places where arrays or ActiveRecord relations could be passed, often calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to_a&lt;/code&gt; just to be safe when we didn’t know what we actually add.  Sorbet has forced us to be explicit about those.  It seems like an improvement in this early days, but it takes away a bit of the Rails magic of just iterating on whatever objects you find.&lt;/p&gt;

&lt;p&gt;Be sure to use the &lt;a href=&quot;https://github.com/chanzuckerberg/sorbet-rails#relationtype-alias&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RelationType&lt;/code&gt;&lt;/a&gt; alias from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet-rails&lt;/code&gt; when dealing with relations so you don’t end up with long &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T.any(...)&lt;/code&gt; signatures for your relation types.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RelationType&lt;/code&gt; combined with &lt;a href=&quot;https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-none&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ActiveRecord#none&lt;/code&gt;&lt;/a&gt; for empty results is that way to happiness.&lt;/p&gt;

&lt;h2 id=&quot;summing-up&quot;&gt;Summing Up&lt;/h2&gt;

&lt;p&gt;Things are going well. We’ve got about 25% of our models that have been specifically typed.  We’ve found some bugs and gotten a bit of refactoring done with increased confidence.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet-rails&lt;/code&gt; has been solid.  We haven’t run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tapioca&lt;/code&gt; yet but expect that’s coming soon.  In a future post we’ll talk about adding types for an ActiveRecord plugin (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;money-rails&lt;/code&gt;).&lt;/p&gt;

&lt;div class=&quot;feedback&quot;&gt;
  &lt;p&gt;
    I&apos;d love to hear any feedback you might have about this post.
    &lt;a href=&quot;https://twitter.com/mrmrbug&quot;&gt;Tweet me up at @mrmrbug&lt;/a&gt;
    or email me at &lt;a href=&quot;mailto:code@dunae.ca&quot;&gt;code@dunae.ca&lt;/a&gt;.
  &lt;/p&gt;

  &lt;p&gt;You can also &lt;a href=&quot;/notes/feed.xml&quot;&gt;grab a little bit of RSS&lt;/a&gt; or &lt;a href=&quot;/notes/&quot;&gt;check out the rest of the blog&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;</content><author><name></name></author><category term="sorbet" /><category term="ruby" /><category term="rails" /><category term="dev" /><summary type="html">Sorbet Journey This is an ongoing series about adding Sorbet to a mature Rails code base. Follow me on Twitter to find out when updates are posted. Part 1: Why Add Types to a Rails App Part 2: Adding Sorbet to an Existing Ruby Gem Part 3: A Typical Day with Adding Sorbet to a Rails App Part 4: Sorbet Stability Part X: Ruby Rogues Podcast</summary></entry><entry><title type="html">Sorbet Journey, Part 2: Adding Sorbet to an Existing Ruby Gem</title><link href="/notes/2020/12/09/adding-sorbet-to-an-existing-gem.html" rel="alternate" type="text/html" title="Sorbet Journey, Part 2: Adding Sorbet to an Existing Ruby Gem" /><published>2020-12-09T14:27:05-08:00</published><updated>2020-12-09T14:27:05-08:00</updated><id>/notes/2020/12/09/adding-sorbet-to-an-existing-gem</id><content type="html" xml:base="/notes/2020/12/09/adding-sorbet-to-an-existing-gem.html">&lt;section class=&quot;blog-toc&quot;&gt;
  &lt;h4&gt;Sorbet Journey&lt;/h4&gt;

  &lt;p&gt;
    This is an ongoing series about adding Sorbet to a mature Rails code base.
    &lt;a href=&quot;https://twitter.com/mrmrbug&quot;&gt;Follow me on Twitter&lt;/a&gt; to find out
    when updates are posted.
  &lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;
      &lt;a href=&quot;/notes/2020/12/01/sorbet-journey-types-motivation.html&quot;&gt;Part 1: Why Add Types to a Rails App&lt;/a&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;a href=&quot;/notes/2020/12/09/adding-sorbet-to-an-existing-gem.html&quot;&gt;Part 2: Adding Sorbet to an Existing Ruby Gem&lt;/a&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;a href=&quot;/notes/2020/12/28/a-typical-day-adding-sorbet-to-rails.html&quot;&gt;Part 3: A Typical Day with Adding Sorbet to a Rails App&lt;/a&gt;
    &lt;/li&gt;

    &lt;li&gt;
      &lt;a href=&quot;/notes/2021/05/25/sorbet-stability.html&quot;&gt;Part 4: Sorbet Stability&lt;/a&gt;
    &lt;/li&gt;

    &lt;li&gt;
      &lt;a href=&quot;/notes/2021/09/15/ruby-rogues-sorbet-podcast.html&quot;&gt;Part X: Ruby Rogues Podcast&lt;/a&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/section&gt;

&lt;aside class=&quot;disclaimer&quot;&gt;
    Head&apos;s up: this is a high level summary of my impressions of working with Sorbet. It assumes you&apos;ve already read the Sorbet docs and are interested in an experience report. It&apos;s not really a tutorial.
&lt;/aside&gt;

&lt;h3 id=&quot;whats-happening&quot;&gt;What’s Happening&lt;/h3&gt;

&lt;p&gt;We chose a super simple gem to use to get a feel for Sorbet in the real world: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tickit-best-seats&lt;/code&gt;.  This gem contains our seat selection algorithm for chosing the “best available” seats &lt;a href=&quot;https://tickit.ca&quot;&gt;when you buy tickets for an event&lt;/a&gt;, following all sorts of business rules.  We chose it because it is largely self-contained, it only uses the Ruby standard library and it works on simple, well-defined data structures.&lt;/p&gt;

&lt;h3 id=&quot;tldr&quot;&gt;TL;DR&lt;/h3&gt;

&lt;p&gt;Overall, &lt;strong&gt;adding Sorbet was an overwhelmingly positive experience.&lt;/strong&gt;  We caught some subtle bugs, tightened up some sloppy data structures and added a lot of safety guarantees.  Some of Sorbet’s current limitations (e.g. lack of splat support) forced a little bit of awkward rewriting.  All in all, the upsides – for this small library – far outweighed the downsides.&lt;/p&gt;

&lt;h2 id=&quot;the-steps&quot;&gt;The Steps&lt;/h2&gt;

&lt;p&gt;Here are a few notes about the process of getting Sorbet wired up.&lt;/p&gt;

&lt;h3 id=&quot;step-cleaning-up-dependencies&quot;&gt;Step: Cleaning Up Dependencies&lt;/h3&gt;

&lt;p&gt;Since Sorbet is going to need to create &lt;a href=&quot;https://sorbet.org/docs/rbi&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RBI&lt;/code&gt; files&lt;/a&gt; for any dependencies I wanted to keep things as clean as possible.  Our gem hadn’t had any active development for about two years, so it was good to get freshened up anyways.&lt;/p&gt;

&lt;p&gt;Almost all the dependencies for this gem are development only.  It uses &lt;a href=&quot;https://github.com/flajann2/juwelier&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;juwelier&lt;/code&gt;&lt;/a&gt; for gem packaging, which unfortunately brings in a lot of other sub-dependencies which ended up causing some issues.&lt;/p&gt;

&lt;p&gt;Our final &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gemspec&lt;/code&gt;…&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_runtime_dependency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;sorbet-runtime-stub&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_development_dependency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;amazing_print&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_development_dependency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;benchmark-ips&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_development_dependency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;bundler&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_development_dependency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;juwelier&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_development_dependency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;minitest&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_development_dependency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;parlour&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_development_dependency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;rubocop&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_development_dependency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;rubocop-performance&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_development_dependency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;rubocop-sorbet&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_development_dependency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;ruby-prof&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_development_dependency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;sorbet&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_development_dependency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;sorbet-runtime&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_development_dependency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;tapioca&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;This modest &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gemspec&lt;/code&gt; resulted in 52 RBI files being brought in, including &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nokogiri&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pry&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hashie&lt;/code&gt; and a bunch of other things we don’t use directly. It’s understandable that they’re needed, but &lt;strong&gt;all the dependent RBIs add visual and mental clutter&lt;/strong&gt; in git and our editors.&lt;/p&gt;

&lt;p&gt;In these early days it was helpful to be able to grep these files.  In future it might be possible to exclude them from our editor config, but &lt;a href=&quot;https://sorbet.org/docs/rbi#a-note-about-vendoring-rbis&quot;&gt;they’ll always be in our repositories&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;step-add-rubocop-sorbet-and-sigils&quot;&gt;Step: Add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rubocop-sorbet&lt;/code&gt; and sigils&lt;/h3&gt;

&lt;p&gt;Shopify’s &lt;a href=&quot;https://github.com/Shopify/rubocop-sorbet&quot;&gt;rubocop-sorbet gem&lt;/a&gt; was helpful to catch a few things before actually bringing in Sorbet.  Our gem is so simple that it didn’t catch much.  The main change was to add all the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;# typed: false&lt;/code&gt; sigils to each file.&lt;/p&gt;

&lt;p&gt;Adding the sigils creates a big messy commit. It wasn’t until later that I discovered the &lt;a href=&quot;https://github.com/sorbet/sorbet/issues/1773&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;default-strictness&lt;/code&gt; option&lt;/a&gt; that was added to Sorbet.  It lets you run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;srb init&lt;/code&gt; without having to touch every single Ruby file.  &lt;strong&gt;Needing to touch every single Ruby file to get started with Sorbet is a definite turn off.&lt;/strong&gt;  Promoting using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;default-strictness&lt;/code&gt; might be a good way to make the first-run experience better.&lt;/p&gt;

&lt;h3 id=&quot;step-wire-up-sorbet&quot;&gt;Step: Wire up Sorbet&lt;/h3&gt;

&lt;p&gt;After seeing Shopify’s talk we were convinced to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tapioca&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;srb rbi&lt;/code&gt; for gem RBIs and it worked pretty well.  The basic steps were:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Comment-out a glitchy gem (see below)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bundle exec srb init&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rm -rf sorbet/rbi/sorbet-typed/&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rm -rf sorbet/rbi/gems/&lt;/code&gt; (since we’re going to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tapioca&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bundle exec tapioca generate&lt;/code&gt; (create all the actual gem RBIs that we will use)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bundle exec srb rbi hidden-definitions&lt;/code&gt; (since these will have changed after running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tapioca&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There was one rough edge with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;srb init&lt;/code&gt;.  The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;juwelier&lt;/code&gt; gem is old and has some problematic dependencies.  We spent the better part of a day trying to chase down answers before realizing it was easier to just comment it out before running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;srb init&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There were two errors we saw.&lt;/p&gt;

&lt;p&gt;The first appears to be because of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;psych&lt;/code&gt; being loaded as a gem:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sorbet/rbi/gems/psych@3.2.0.rbi:223: Method Psych::Nodes::Node#to_ruby redefined without matching argument count. Expected: 0, got: 2 https://srb.help/4010
     223 |  def to_ruby(symbolize_names: T.unsafe(nil), freeze: T.unsafe(nil)); end
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    https://github.com/sorbet/sorbet/tree/3a01f6f45b1760e255239166e76db90c33690bfb/rbi/stdlib/psych.rbi#L875: Previous definition
     875 |  def to_ruby(); end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;psych&lt;/code&gt; is in the Ruby standard library, but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;srb init&lt;/code&gt; attempted to create separate gem definitions, causing these &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Previous definition&lt;/code&gt; errors.&lt;/p&gt;

&lt;p&gt;The second was about a namespace already being defined (which we never bothered to diagnose further):&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;.../gems/github_api-0.19.0/lib/github_api/api.rb:328:in `namespace&apos;: namespace &apos;say&apos; is already defined (ArgumentError)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Commenting out the gem was thankfully easy in this case. If it hadn’t been we would likely have either forked our glitchy dependencies or chosen new ones.&lt;/p&gt;

&lt;h3 id=&quot;step-configure-sorbet-runtime-stub&quot;&gt;Step: Configure &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet-runtime-stub&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;We only wanted Sorbet to strictly enforce types during development while we’re still evaluating the toolchain.  Shopify’s &lt;a href=&quot;https://github.com/Shopify/sorbet-runtime-stub&quot;&gt;sorbet-runtime-stub library&lt;/a&gt; let’s that happen.  It stubs out all of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet-runtime&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T&lt;/code&gt; methods as no-ops, so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T.cast&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T.must&lt;/code&gt;, for example, don’t have any effect.&lt;/p&gt;

&lt;p&gt;We only want it to run these stubs in the released “production” gem so we add this to the top of our main library code:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;sorbet-runtime-stub&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;defined?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;In order to still get the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet-runtime&lt;/code&gt; checks in development you can &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;require &apos;sorbet-runtime&apos;&lt;/code&gt; in your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Rakefile&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test_helper.rb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;(After going down this route we discovered &lt;a href=&quot;https://github.com/sorbet/sorbet/commit/d637d2f5986ba9668ab725cdc13a7cd0cdf124a3&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T::Sig::WithoutRuntime&lt;/code&gt;&lt;/a&gt; which looks like another useful tool for avoiding runtime checks in dependencies.)&lt;/p&gt;

&lt;h2 id=&quot;code-changes&quot;&gt;Code Changes&lt;/h2&gt;

&lt;p&gt;Here are some of the bits of code that actually changed as a result of adding Sorbet.&lt;/p&gt;

&lt;h3 id=&quot;data-structure-improvements&quot;&gt;Data Structure Improvements&lt;/h3&gt;

&lt;p&gt;The seat search results are passed around as an array like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[SearchScore, [SeatObject, SeatObject, SeatObject]]&lt;/code&gt;.  The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;score&lt;/code&gt; could be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;false&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nil&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Integer&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Float&lt;/code&gt; depending on the context.&lt;/p&gt;

&lt;p&gt;This was never ideal, but defining Sorbet’s types made it plain just how silly it was.  No one wants to write &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T.nilable(T.any(T::Boolean, Integer, Float))&lt;/code&gt; all the time.  Instead we used a non-nullable float, using the much more elegant &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-Float::INFINITY&lt;/code&gt; for “no results”. Our data structures got tighter and we got to remove some conditionals at the same time.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;&lt;span class=&quot;gd&quot;&gt;-  NO_RESULTS = [false, []].freeze
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+  NO_RESULTS = [-Float::INFINITY, []].freeze&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;more-explicit-constructors&quot;&gt;More Explicit Constructors&lt;/h3&gt;

&lt;p&gt;While it wasn’t strictly necessary, we ended up being much more explicit with all of our initializers, even when added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;typed: true&lt;/code&gt; strictness where typed initializers are not actually required.&lt;/p&gt;

&lt;p&gt;Adding these types added a bunch of ceremony to the top of each class, but the benefits of explicit types everywhere seemed to be worth it.&lt;/p&gt;

&lt;h4 id=&quot;before-quick-class-initialization&quot;&gt;Before (Quick Class Initialization)&lt;/h4&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Seat&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;attr_reader&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:seat_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:parent&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;initialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seat_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;vi&quot;&gt;@seat_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seat_id&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4 id=&quot;after-more-explicit-class-initialization&quot;&gt;After (More Explicit Class Initialization)&lt;/h4&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Seat&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;sig&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;returns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;attr_reader&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:seat_id&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;sig&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;returns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;nilable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SeatContainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;attr_reader&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:parent&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;sig&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
       &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
         &lt;span class=&quot;ss&quot;&gt;seat_id: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
         &lt;span class=&quot;ss&quot;&gt;parent:  &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;nilable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SeatContainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
       &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;void&lt;/span&gt;
     &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;initialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seat_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;vi&quot;&gt;@seat_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seat_id&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;polymorphism-is-not-as-fun&quot;&gt;Polymorphism Is Not As Fun&lt;/h3&gt;

&lt;p&gt;Our seat maps are made of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SeatContainer&lt;/code&gt; objects with contain either more &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SeatContainer&lt;/code&gt; objects, or actual &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Seat&lt;/code&gt;s.  In Sorbet that looks like:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SeatContainer&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;sig&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;returns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SeatContainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Seat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;attr_reader&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:children&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Before Sorbet that served us really well.  Duck-typing made things work nicely and we recursed by checking &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;parent?&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;children?&lt;/code&gt; methods quite elegantly.&lt;/p&gt;

&lt;p&gt;After our first pass with Sorbet this wasn’t working well; Sorbet really wanted to know if it was either a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SeatContainer&lt;/code&gt; or a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Seat&lt;/code&gt;.  The fastest way forward (remembering this is still our proof-of-concept) was to add lookup methods that filtered and cast the child elements before returning the results, e.g.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SeatContainer&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;sig&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;returns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;nilable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Seat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;seat_children&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;child_index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;is_seat_container?&lt;/span&gt;
        &lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;children&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;child_index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Seat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Pretty clunky.  I’m sure that Sorbet’s abstract types would help us out, but we didn’t get that far during our first experiments.&lt;/p&gt;

&lt;h3 id=&quot;splats-with-uknown-array-sizes&quot;&gt;Splats with Uknown Array Sizes&lt;/h3&gt;

&lt;p&gt;This might be a super specific limitation to our codebase, but I think illustrates the pitfalls of using a still-in-development toolchain.  We used to call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;values_at(*some_indexes)&lt;/code&gt; but &lt;a href=&quot;https://sorbet.org/docs/error-reference#7019&quot;&gt;Sorbet does not support splats with variable numbers of arguments&lt;/a&gt;. So we had to do some rewriting:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;&lt;span class=&quot;gd&quot;&gt;-    seats = children.values_at(*chunk.indexes)
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+    seats = seats_at_indexes(chunk.indexes)
+
+    sig { params(indexes: T::Array[Integer]).returns(T::Array[Seat]) }
+    def seats_at_indexes(indexes)
+      indexes.map do |index|
+        children[index]
+      end.compact
+    end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;creating-our-gems-rbi-file-with-parlour&quot;&gt;Creating our Gem’s RBI File with Parlour&lt;/h3&gt;

&lt;p&gt;Since we are already doing all the type definition work it seemed our final step should be to generate our own RBI files to ship with the gem.  Otherwise, when we use our gem we’d be relying on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tapioca&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hidden-definitions&lt;/code&gt; when we really shouldn’t need to.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/AaronC81/parlour&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;parlour&lt;/code&gt;&lt;/a&gt; seemed to be the tool we needed.  I was hesitant to add more dependencies and it wasn’t clear what its role is in the Sorbet ecosystem.  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;parlour&lt;/code&gt; is used &lt;em&gt;heavily&lt;/em&gt; by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tapioca&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sorbet-rails&lt;/code&gt; and &lt;a href=&quot;https://twitter.com/OrangeFlash81/status/1337476442746216457&quot;&gt;has received PRs from Shopify&lt;/a&gt; so it’s sort-of blessed.  Blessed enough for us, at least.&lt;/p&gt;

&lt;p&gt;It was super easy to run: we created a &lt;a href=&quot;https://github.com/AaronC81/parlour/wiki/The-.parlour-file&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.parlour&lt;/code&gt; file&lt;/a&gt; and ran &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bundle exec parlour&lt;/code&gt; and it was done.&lt;/p&gt;

&lt;p&gt;Including RBI files with gems is pretty lightly documented and still feels like a work in progress.  We ran into a few issues once we started using the gem in the Rails app, but we’ll save those for the next post.&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h2&gt;

&lt;p&gt;Starting our Sorbet journey with an isolated gem was a useful excercise. It got us comfortable with Sorbet syntax and concepts (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T.must&lt;/code&gt; for nil checks) without needing to wrangle an entire Rails meta-programmed monolith.  It also let us experiment with different parts of the toolchain (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;srb rbi&lt;/code&gt; vs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tapioca&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Sorbet checks are now a required step in this gem’s CI test suite and the Sorbet-ified version of our gem is now live.  We’ve already started applying the lessons learned here to our main app. Stay tuned for that post.&lt;/p&gt;

&lt;div class=&quot;feedback&quot;&gt;
  &lt;p&gt;
    I&apos;d love to hear any feedback you might have about this post.
    &lt;a href=&quot;https://twitter.com/mrmrbug&quot;&gt;Tweet me up at @mrmrbug&lt;/a&gt;
    or email me at &lt;a href=&quot;mailto:code@dunae.ca&quot;&gt;code@dunae.ca&lt;/a&gt;.
  &lt;/p&gt;

  &lt;p&gt;You can also &lt;a href=&quot;/notes/feed.xml&quot;&gt;grab a little bit of RSS&lt;/a&gt; or &lt;a href=&quot;/notes/&quot;&gt;check out the rest of the blog&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;</content><author><name></name></author><category term="sorbet" /><category term="ruby" /><category term="rails" /><category term="dev" /><summary type="html">Sorbet Journey This is an ongoing series about adding Sorbet to a mature Rails code base. Follow me on Twitter to find out when updates are posted. Part 1: Why Add Types to a Rails App Part 2: Adding Sorbet to an Existing Ruby Gem Part 3: A Typical Day with Adding Sorbet to a Rails App Part 4: Sorbet Stability Part X: Ruby Rogues Podcast</summary></entry><entry><title type="html">Sorbet Journey, Part 1: Why Add Types to a Rails App</title><link href="/notes/2020/12/01/sorbet-journey-types-motivation.html" rel="alternate" type="text/html" title="Sorbet Journey, Part 1: Why Add Types to a Rails App" /><published>2020-12-01T14:27:05-08:00</published><updated>2020-12-01T14:27:05-08:00</updated><id>/notes/2020/12/01/sorbet-journey-types-motivation</id><content type="html" xml:base="/notes/2020/12/01/sorbet-journey-types-motivation.html">&lt;section class=&quot;blog-toc&quot;&gt;
  &lt;h4&gt;Sorbet Journey&lt;/h4&gt;

  &lt;p&gt;
    This is an ongoing series about adding Sorbet to a mature Rails code base.
    &lt;a href=&quot;https://twitter.com/mrmrbug&quot;&gt;Follow me on Twitter&lt;/a&gt; to find out
    when updates are posted.
  &lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;
      &lt;a href=&quot;/notes/2020/12/01/sorbet-journey-types-motivation.html&quot;&gt;Part 1: Why Add Types to a Rails App&lt;/a&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;a href=&quot;/notes/2020/12/09/adding-sorbet-to-an-existing-gem.html&quot;&gt;Part 2: Adding Sorbet to an Existing Ruby Gem&lt;/a&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;a href=&quot;/notes/2020/12/28/a-typical-day-adding-sorbet-to-rails.html&quot;&gt;Part 3: A Typical Day with Adding Sorbet to a Rails App&lt;/a&gt;
    &lt;/li&gt;

    &lt;li&gt;
      &lt;a href=&quot;/notes/2021/05/25/sorbet-stability.html&quot;&gt;Part 4: Sorbet Stability&lt;/a&gt;
    &lt;/li&gt;

    &lt;li&gt;
      &lt;a href=&quot;/notes/2021/09/15/ruby-rogues-sorbet-podcast.html&quot;&gt;Part X: Ruby Rogues Podcast&lt;/a&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/section&gt;

&lt;h3 id=&quot;tldr&quot;&gt;TL;DR&lt;/h3&gt;

&lt;p&gt;Working on a big code base without types is hard. We’ve been interested in Sorbet for a while but were waiting for the “right time.” As of late 2020 we’re going to give it a real try.&lt;/p&gt;

&lt;h3 id=&quot;whats-the-problem&quot;&gt;What’s the Problem&lt;/h3&gt;

&lt;p&gt;I oversee a ten-year-old “majestic monolith” Rails application. The app is &lt;a href=&quot;https://tickit.ca/&quot;&gt;Tickit&lt;/a&gt;, an event ticket sales system (a competitor to Eventbrite and Brown Paper Tickets). It deals with money in various currencies, events in various timezones, sales over various channels, and generates tickets according to various rules.&lt;/p&gt;

&lt;p&gt;The app is around 100k lines of code and it is decently factored given its age.  There are lots of high quality tests.  Even so, it’s been &lt;strong&gt;getting harder and harder to change the app with confidence&lt;/strong&gt;.  A typical change to any business logic involves re-building a mental map of the data flowing from storefront to the backend, often wrapped up in hashes like this:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;mi&quot;&gt;12356&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;quantity: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;freeform_base_price: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;125.50&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;delivery: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;digital&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;mi&quot;&gt;44123&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;quantity: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;delivery: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;digital&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;And then they get decorated with payment requirements…&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;cart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;grand_total&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; Money.new(123_45, &apos;USD&apos;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;display_fees&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; Money.zero(&apos;USD&apos;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Which are sometimes modified as cents…&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;cart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;gratuity_cents&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10_00&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;And payment methods…&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;payment_source: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;payment_type: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;apple_pay&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;And seat selection…&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;seat_selection_mode: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;manual&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;seat_qids: &lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%w[e1-a e1-b]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Every request involves building up data structures from request payloads, passing them down through the business logic and back to the user, transforming them as we go.  &lt;strong&gt;At every step we need to be sure that all the transforms that have happened are typesafe &lt;em&gt;and&lt;/em&gt; appropriate.&lt;/strong&gt; Pretty standard stuff, but time consuming nonetheless.&lt;/p&gt;

&lt;p&gt;Our automated tests were extensive and gave us confidence, but a full CI run can take ten minutes and that feedback loop is too long.  &lt;strong&gt;We want to be warned immediately if we try to put a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Money&lt;/code&gt; object where an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Integer&lt;/code&gt; is meant to be.&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;we-got-a-taste-of-typescript&quot;&gt;We Got a Taste of Typescript&lt;/h3&gt;

&lt;p&gt;We started converting our vanilla JS and CoffeeScript over to TypeScript about a year ago and it has been a fantastic win.  Other than some tooling issues, day-to-day development using TypeScript has added a huge layer of confidence and increased code quality.&lt;/p&gt;

&lt;p&gt;An unexpected bonus was IDE integration: after years of being a Sublime Text minimalist, moving to VS Code and getting auto-complete when writing TypeScript has been amazing.&lt;/p&gt;

&lt;p&gt;Anytime we’ve had to write JS instead of TypeScript it has felt like a big step backwards. Writing Ruby without types was starting to feel the same, too.&lt;/p&gt;

&lt;h3 id=&quot;just-add-types&quot;&gt;Just Add Types&lt;/h3&gt;

&lt;p&gt;We experimented with Sorbet when it first came out, but found too many rough edges when adding it to our code base. There was a lot we were excited about, but we didn’t have the resources to pursue it then.  We filed it away for future exploration.&lt;/p&gt;

&lt;p&gt;This November, Shopify published a &lt;a href=&quot;https://shopify.engineering/adopting-sorbet&quot;&gt;blog post about adopting Sorbet&lt;/a&gt; and hosted a webinar focusing on their &lt;a href=&quot;https://github.com/Shopify/tapioca&quot;&gt;Tapioca library&lt;/a&gt;.  If Shopify can run Rails master and Sorbet, we figured, maybe it was ready for us to try with our more, uh, modest resources.&lt;/p&gt;

&lt;p&gt;Rather than starting with our main app, we decided to start with a single private gem of ours to get a feel for Sorbet at its various strictness levels. If that went well, we could look at adding Sorbet to the main Rails app.  &lt;a href=&quot;/notes/2020/12/09/adding-sorbet-to-an-existing-gem.html&quot;&gt;So that’s where we began.&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;feedback&quot;&gt;
  &lt;p&gt;
    I&apos;d love to hear any feedback you might have about this post.
    &lt;a href=&quot;https://twitter.com/mrmrbug&quot;&gt;Tweet me up at @mrmrbug&lt;/a&gt;
    or email me at &lt;a href=&quot;mailto:code@dunae.ca&quot;&gt;code@dunae.ca&lt;/a&gt;.
  &lt;/p&gt;

  &lt;p&gt;You can also &lt;a href=&quot;/notes/feed.xml&quot;&gt;grab a little bit of RSS&lt;/a&gt; or &lt;a href=&quot;/notes/&quot;&gt;check out the rest of the blog&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;</content><author><name></name></author><category term="sorbet" /><category term="ruby" /><category term="rails" /><category term="dev" /><summary type="html">Sorbet Journey This is an ongoing series about adding Sorbet to a mature Rails code base. Follow me on Twitter to find out when updates are posted. Part 1: Why Add Types to a Rails App Part 2: Adding Sorbet to an Existing Ruby Gem Part 3: A Typical Day with Adding Sorbet to a Rails App Part 4: Sorbet Stability Part X: Ruby Rogues Podcast</summary></entry></feed>