<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>House Absolute(ly Pointless)</title><link>https://blog.urth.org/</link><description>Recent content on House Absolute(ly Pointless)</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sun, 08 Mar 2026 23:51:03 -0500</lastBuildDate><atom:link href="https://blog.urth.org/index.xml" rel="self" type="application/rss+xml"/><item><title>Who Says Recruiting Is Hard?</title><link>https://blog.urth.org/2026/03/08/who-says-recruiting-is-hard/</link><pubDate>Sun, 08 Mar 2026 23:51:03 -0500</pubDate><guid>https://blog.urth.org/2026/03/08/who-says-recruiting-is-hard/</guid><description>&lt;p>Name redacted because I&amp;rsquo;m a nice person?&lt;/p>
&lt;p>From an anonymous recruiter at Datacurve (YC&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> W24):&lt;/p>
&lt;blockquote>
&lt;p>Hey Dave! I saw your fix for typo in NodeFactory.bump docs on the Dioxus web framework and thought
you&amp;rsquo;d be perfect for a project I&amp;rsquo;m running where we collaborate with top SWEs to push the
capabilities of leading LLMs. Want to hear more?&lt;/p>
&lt;/blockquote>
&lt;p>Yes, &amp;ldquo;my fix for typo&amp;rdquo; does mean I&amp;rsquo;m the perfect person for this project that you (the recruiter?)
are running. I guess they haven&amp;rsquo;t quite pushed the capabilities of leading LLMS far enough to A)
have decent grammar; B) realize that my &amp;ldquo;fix for typo&amp;rdquo; is not any indication of programming prowess.&lt;/p>
&lt;p>&lt;a href="https://github.com/DioxusLabs/dioxus/commit/69752313eb49ba4a9152cd9dcd8bbf5db507a200">Here is the commit&lt;/a>
in all its glory! Spoiler, it&amp;rsquo;s literally a two byte fix.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>They must be so proud!&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>How Good is Claude at Finding Bugs in My Code?</title><link>https://blog.urth.org/2025/10/25/how-good-is-claude-at-finding-bugs-in-my-code/</link><pubDate>Sat, 25 Oct 2025 10:37:44 -0500</pubDate><guid>https://blog.urth.org/2025/10/25/how-good-is-claude-at-finding-bugs-in-my-code/</guid><description>&lt;p>I recently tried using Claude to look at &lt;a href="https://github.com/houseabsolute/ubi/">my &lt;code>ubi&lt;/code> project&lt;/a>
and tell me about any bugs it found. To avoid having to pay for Claude Code, I used the web API and
uploaded a tarball of &lt;a href="https://github.com/houseabsolute/ubi/tree/master/ubi">the &lt;code>ubi&lt;/code> crate&lt;/a>, which
contains all the important bits of &lt;code>ubi&lt;/code>.&lt;/p>
&lt;p>I was curious to see how well it would do. AI tools for coding have improved a &lt;em>lot&lt;/em> in the past
year (and probably in the past month and week). Would it find a lot of bugs I&amp;rsquo;d never noticed, or
would it be full of nonsense?&lt;/p>
&lt;p>Here&amp;rsquo;s the full report it produced:&lt;/p>
&lt;details style="background-color: #eee; padding: 0 1em 0 1em">
&lt;summary>
&lt;h1>Bug Analysis: UBI Asset Selection Logic&lt;/h1>
&lt;h2 id="overview">Overview&lt;/h2>
&lt;p>This document contains a detailed analysis of potential bugs and issues in the ubi (Universal Binary
Installer) project&amp;rsquo;s asset selection logic, based on reviewing the source code in &lt;code>picker.rs&lt;/code>,
&lt;code>installer.rs&lt;/code>, &lt;code>archive.rs&lt;/code>, &lt;code>os.rs&lt;/code>, and &lt;code>arch.rs&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Click to expand &amp;hellip;&lt;/strong>&lt;/p>
&lt;/summary>
&lt;hr>
&lt;h2 id="critical-bugs">Critical Bugs&lt;/h2>
&lt;h3 id="1-android-detection-too-broad-linux-arm64-confusion">1. &lt;strong>Android Detection Too Broad, Linux ARM64 Confusion&lt;/strong>&lt;/h3>
&lt;p>&lt;strong>Location:&lt;/strong> &lt;code>os.rs&lt;/code>, line 7 and &lt;code>picker.rs&lt;/code>, lines 170-173&lt;/p>
&lt;p>&lt;strong>Issue:&lt;/strong> The Android regex is simply &lt;code>(?i:android)&lt;/code> without word boundaries, and the special-case
filtering only applies when &lt;code>platform.target_os != OS::Android&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">os_matcher&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">is_match&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">asset&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">platform&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">target_os&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">!=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="no">OS&lt;/span>::&lt;span class="n">Android&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&amp;amp;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">android_re&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">is_match&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">asset&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">debug!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;does not match our OS&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">continue&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>Problems:&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>The Android regex can match filenames containing &amp;ldquo;android&amp;rdquo; anywhere, even as part of another word&lt;/li>
&lt;li>The logic assumes Linux matches will come first, then filters Android, but both might match&lt;/li>
&lt;li>A file like &lt;code>myapp-armandroid-linux.tar.gz&lt;/code> might be incorrectly filtered&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Impact:&lt;/strong> This caused the bug mentioned in release notes where ubi incorrectly picked Android
releases on Linux aarch64.&lt;/p>
&lt;p>&lt;strong>Fix:&lt;/strong> Use word boundaries for Android regex: &lt;code>regex!(r&amp;quot;(?i:(?:\b|_)android(?:\b|_))&amp;quot;)&lt;/code> and ensure
proper OS precedence.&lt;/p>
&lt;hr>
&lt;h3 id="2-exact-match-doesnt-check-executability--critical">2. &lt;strong>Exact Match Doesn&amp;rsquo;t Check Executability&lt;/strong> 🔴 CRITICAL&lt;/h3>
&lt;p>&lt;strong>Location:&lt;/strong> &lt;code>installer.rs&lt;/code>, lines 368-370&lt;/p>
&lt;p>&lt;strong>Issue:&lt;/strong> When an exact filename match is found in an archive, the code immediately returns it
WITHOUT checking if the file is executable:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">archive_member_is_exact_match&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">file_name&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">debug!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;found {archive_type} file entry with exact match: `{file_name}`&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Ok&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Returns immediately, no executable check!
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This is even documented in the README: &amp;ldquo;Note that if it finds an exact match, it does not check the
file&amp;rsquo;s mode.&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Impact:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>On Unix systems, if an archive contains both a non-executable script and an executable binary with
the project name, the non-executable might be selected&lt;/li>
&lt;li>A malicious or poorly structured archive could have a non-executable data file with the right name
that gets selected over an actual executable&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Example Failure:&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">archive/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── myapp (mode 644, data file)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── myapp-linux-x86_64 (mode 755, actual executable)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The non-executable &lt;code>myapp&lt;/code> would be selected.&lt;/p>
&lt;p>&lt;strong>Fix:&lt;/strong> Always check executability, even for exact matches on Unix systems.&lt;/p>
&lt;hr>
&lt;h3 id="3-arm-regex-too-permissive-on-macos">3. &lt;strong>ARM Regex Too Permissive on macOS&lt;/strong>&lt;/h3>
&lt;p>&lt;strong>Location:&lt;/strong> &lt;code>arch.rs&lt;/code>, lines 40-62&lt;/p>
&lt;p>&lt;strong>Issue:&lt;/strong> The &lt;code>macos_aarch64_only_re()&lt;/code> includes just &lt;code>arm&lt;/code> as a match:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="p">(&lt;/span>&lt;span class="o">?&lt;/span>:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nc">aarch_&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="mi">64&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">arm_&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="mi">64&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">arm&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Too broad!
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>Impact:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Will match &lt;code>armv7&lt;/code>, &lt;code>armhf&lt;/code>, &lt;code>arm-linux&lt;/code>, &lt;code>armchair&lt;/code>, etc.&lt;/li>
&lt;li>Could incorrectly select 32-bit ARM binaries on ARM64 macOS&lt;/li>
&lt;li>Recent fix changed from &amp;ldquo;arm64&amp;rdquo; to &amp;ldquo;arm&amp;rdquo;, making it even broader&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Example:&lt;/strong> A file named &lt;code>myapp-darwin-armv7.tar.gz&lt;/code> would match on macOS ARM64.&lt;/p>
&lt;p>&lt;strong>Fix:&lt;/strong> Be more specific: require either 64 indicators or use negative lookahead to exclude 32-bit
variants.&lt;/p>
&lt;hr>
&lt;h2 id="significant-bugs">Significant Bugs&lt;/h2>
&lt;h3 id="4-64-bit-filter-uses-naive-string-search--high-severity">4. &lt;strong>64-bit Filter Uses Naive String Search&lt;/strong> ⚠️ HIGH SEVERITY&lt;/h3>
&lt;p>&lt;strong>Location:&lt;/strong> &lt;code>picker.rs&lt;/code>, lines 322-335&lt;/p>
&lt;p>&lt;strong>Issue:&lt;/strong> The 64-bit filtering uses a simple substring search for &amp;ldquo;64&amp;rdquo;:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="n">matches&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">iter&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">any&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;64&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">debug!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;no 64-bit assets found, falling back to all assets&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">matches&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sixty_four_bit&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">matches&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">into_iter&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;64&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">collect&lt;/span>::&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>Problems:&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Matches version numbers&lt;/strong>: &lt;code>myapp-v1.64.2-linux-i686.tar.gz&lt;/code> would be considered 64-bit even
though it&amp;rsquo;s i686 (32-bit)&lt;/li>
&lt;li>&lt;strong>Matches years&lt;/strong>: &lt;code>myapp-2064-edition-arm32.tar.gz&lt;/code> would be considered 64-bit&lt;/li>
&lt;li>&lt;strong>Matches hashes/IDs&lt;/strong>: &lt;code>myapp-build-abc64def-armv7.tar.gz&lt;/code> would be considered 64-bit&lt;/li>
&lt;li>&lt;strong>Matches other contexts&lt;/strong>: &lt;code>sha64&lt;/code>, &lt;code>base64&lt;/code>, &lt;code>sha256464&lt;/code>, etc.&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Impact:&lt;/strong> Can incorrectly include 32-bit binaries in the &amp;ldquo;64-bit&amp;rdquo; filter results. While x86_64 can
run 32-bit x86 binaries, this breaks down for other architectures:&lt;/p>
&lt;ul>
&lt;li>On ARM64, should prefer &lt;code>aarch64&lt;/code> over &lt;code>arm32&lt;/code>&lt;/li>
&lt;li>On x86_64, should prefer native 64-bit over 32-bit compatibility mode&lt;/li>
&lt;li>The wrong binary might be selected based on alphabetical ordering if both pass through&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Example Failure Scenarios:&lt;/strong>&lt;/p>
&lt;p>Scenario 1 - Version number interference:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Platform: aarch64 (ARM64)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Assets:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - myapp-v1.64.0-linux-armv7.tar.gz (contains &amp;#34;64&amp;#34;, but is 32-bit ARM!)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - myapp-v1.63.0-linux-aarch64.tar.gz (no &amp;#34;64&amp;#34; in version, but IS 64-bit!)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Result: Selects the 32-bit ARM binary because it has &amp;#34;64&amp;#34; in version number
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Scenario 2 - Build IDs:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Platform: x86_64
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Assets:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - myapp-linux-i686-build642.tar.gz (contains &amp;#34;64&amp;#34;, but is 32-bit)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - myapp-linux-x86_64.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Result: Both pass the filter, then alphabetical sort picks one arbitrarily
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>Proper Fix - Architecture-Specific 64-bit Detection:&lt;/strong>&lt;/p>
&lt;p>Replace the naive string search with architecture-aware regex matching:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fn&lt;/span> &lt;span class="nf">maybe_filter_for_64_bit_arch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">matches&lt;/span>: &lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Asset&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Asset&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Only filter on 64-bit architectures
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="fm">matches!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">platform&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">target_arch&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">AArch64&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">Mips64&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">PowerPc64&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">Riscv64&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="no">S390X&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">Sparc64&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">X86_64&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">matches&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">asset_names&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">matches&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">iter&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">map&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">as_str&lt;/span>&lt;span class="p">()).&lt;/span>&lt;span class="n">collect&lt;/span>::&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">debug!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;found multiple candidate assets on 64-bit platform, filtering for 64-bit binaries in {asset_names:?}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Create architecture-specific 64-bit indicator regex
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">bit64_re&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">arch_64_bit_indicator_regex&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sixty_four_bit&lt;/span>: &lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Asset&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">matches&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">iter&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">bit64_re&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">is_match&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">cloned&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">collect&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sixty_four_bit&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">is_empty&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">debug!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;no assets explicitly marked as 64-bit found, falling back to all assets&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">matches&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">debug!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;found 64-bit assets: {}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sixty_four_bit&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">iter&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">map&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">as_str&lt;/span>&lt;span class="p">()).&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;,&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sixty_four_bit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">fn&lt;/span> &lt;span class="nf">arch_64_bit_indicator_regex&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="nc">Regex&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Match architecture-specific 64-bit indicators near word boundaries
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pattern&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">match&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">platform&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">target_arch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">X86_64&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="sa">r&lt;/span>&lt;span class="s">&amp;#34;(?i)(?:\b|_)(?:x86[_-]?64|x64|amd64)(?:\b|_)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">AArch64&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="sa">r&lt;/span>&lt;span class="s">&amp;#34;(?i)(?:\b|_)(?:aarch64|arm64)(?:\b|_)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">Mips64&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="sa">r&lt;/span>&lt;span class="s">&amp;#34;(?i)(?:\b|_)mips64(?:el|le)?(?:\b|_)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">PowerPc64&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="sa">r&lt;/span>&lt;span class="s">&amp;#34;(?i)(?:\b|_)(?:ppc64|powerpc64)(?:le)?(?:\b|_)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">Riscv64&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="sa">r&lt;/span>&lt;span class="s">&amp;#34;(?i)(?:\b|_)riscv64(?:\b|_)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="no">S390X&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="sa">r&lt;/span>&lt;span class="s">&amp;#34;(?i)(?:\b|_)s390x(?:\b|_)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">Sparc64&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="sa">r&lt;/span>&lt;span class="s">&amp;#34;(?i)(?:\b|_)sparc64(?:\b|_)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="sa">r&lt;/span>&lt;span class="s">&amp;#34;(?i)(?:\b|_)64(?:\b|_)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Fallback for unknown 64-bit architectures
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Regex&lt;/span>::&lt;span class="n">new&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">pattern&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="n">unwrap&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>Alternative Simpler Fix&lt;/strong> (if full regex solution is too complex):&lt;/p>
&lt;p>Use a more restrictive substring search that requires word boundaries around &amp;ldquo;64&amp;rdquo;:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fn&lt;/span> &lt;span class="nf">maybe_filter_for_64_bit_arch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">matches&lt;/span>: &lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Asset&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Asset&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="fm">matches!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">platform&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">target_arch&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">AArch64&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">Mips64&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">PowerPc64&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">Riscv64&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="no">S390X&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">Sparc64&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Arch&lt;/span>::&lt;span class="n">X86_64&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">matches&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">asset_names&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">matches&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">iter&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">map&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">as_str&lt;/span>&lt;span class="p">()).&lt;/span>&lt;span class="n">collect&lt;/span>::&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">debug!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;found multiple candidate assets, filtering for 64-bit binaries in {asset_names:?}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Use a regex that requires 64 to be part of an architecture indicator,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// not just anywhere in the string
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">bit64_re&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="fm">regex!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">r&lt;/span>&lt;span class="s">&amp;#34;(?i)(?:\b|_)(?:\w*64|64\w+)(?:\b|_)&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sixty_four_bit&lt;/span>: &lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Asset&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">matches&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">iter&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name_lower&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">to_lowercase&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Must contain &amp;#34;64&amp;#34; with word boundaries, not in version numbers
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Version number pattern: vX.64.Y or vX.Y.64
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">is_version&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="fm">regex!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">r&lt;/span>&lt;span class="s">&amp;#34;v\d+\.\d*64&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="n">is_match&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">name_lower&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">bit64_re&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">is_match&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&amp;amp;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="n">is_version&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">})&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">cloned&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">collect&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sixty_four_bit&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">is_empty&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">debug!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;no 64-bit assets found, falling back to all assets&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">matches&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">debug!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;found 64-bit assets: {}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sixty_four_bit&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">iter&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">map&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">as_str&lt;/span>&lt;span class="p">()).&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;,&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sixty_four_bit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>Benefits of the Fix:&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>Architecture-specific matching prevents false positives&lt;/li>
&lt;li>Explicitly excludes version numbers like &amp;ldquo;1.64.0&amp;rdquo;&lt;/li>
&lt;li>Requires &amp;ldquo;64&amp;rdquo; to be part of an architecture indicator (word boundaries)&lt;/li>
&lt;li>Falls back gracefully if no 64-bit indicators found&lt;/li>
&lt;li>More maintainable - uses existing architecture regexes&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Testing Required:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>myapp-v1.64.0-linux-i686.tar.gz&lt;/code> should NOT match on x86_64&lt;/li>
&lt;li>&lt;code>myapp-linux-x86_64.tar.gz&lt;/code> SHOULD match on x86_64&lt;/li>
&lt;li>&lt;code>myapp-aarch64-darwin.tar.gz&lt;/code> SHOULD match on ARM64&lt;/li>
&lt;li>&lt;code>myapp-arm64-v2.64.tar.gz&lt;/code> SHOULD match on ARM64 (arm64 indicator present, version ignored)&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="5-mips-regex-ordering-issue">5. &lt;strong>MIPS Regex Ordering Issue&lt;/strong>&lt;/h3>
&lt;p>&lt;strong>Location:&lt;/strong> &lt;code>arch.rs&lt;/code>, lines 122-138 and 158-174&lt;/p>
&lt;p>&lt;strong>Issue:&lt;/strong> The &lt;code>mips_re()&lt;/code> regex will match &lt;code>mips64&lt;/code> because it just looks for &lt;code>mips&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">fn&lt;/span> &lt;span class="nf">mips_re&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="nb">&amp;#39;static&lt;/span> &lt;span class="nc">Lazy&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Regex&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">regex!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">r&lt;/span>&lt;span class="s">&amp;#34;(?ix)(?:\b|_)mips(?:\b|_)&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Matches &amp;#34;mips64&amp;#34;!
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>Impact:&lt;/strong> On a 32-bit MIPS platform, it might match 64-bit assets. The code relies on checking
&lt;code>mips64&lt;/code> first, but if regexes are applied in wrong order, this fails.&lt;/p>
&lt;p>&lt;strong>Fix:&lt;/strong> Add negative lookahead: &lt;code>mips(?!64)&lt;/code> or be more careful about ordering.&lt;/p>
&lt;hr>
&lt;h3 id="6-musl-detection-relies-on-external-binary">6. &lt;strong>Musl Detection Relies on External Binary&lt;/strong>&lt;/h3>
&lt;p>&lt;strong>Location:&lt;/strong> Mentioned in docs; detection happens before picker&lt;/p>
&lt;p>&lt;strong>Issue:&lt;/strong> The musl detection runs &lt;code>ldd /bin/ls&lt;/code> to check for musl.&lt;/p>
&lt;p>&lt;strong>Problems:&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>&lt;code>/bin/ls&lt;/code> might not exist on minimal systems or non-FHS systems&lt;/li>
&lt;li>&lt;code>ldd&lt;/code> might not be in PATH&lt;/li>
&lt;li>The command might fail for other reasons&lt;/li>
&lt;li>Users can&amp;rsquo;t easily override false negatives&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Impact:&lt;/strong> On musl systems without &lt;code>/bin/ls&lt;/code>, detection fails and glibc binaries might be selected.&lt;/p>
&lt;p>&lt;strong>Fix:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Check multiple common binaries&lt;/li>
&lt;li>Provide better fallback mechanisms&lt;/li>
&lt;li>Make the override mechanism more discoverable&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="7-libc-filtering-is-too-lenient">7. &lt;strong>Libc Filtering is Too Lenient&lt;/strong>&lt;/h3>
&lt;p>&lt;strong>Location:&lt;/strong> &lt;code>picker.rs&lt;/code>, lines 235-258&lt;/p>
&lt;p>&lt;strong>Issue:&lt;/strong> The libc matching allows assets without any libc indicator:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">asset&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;-gnu&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">asset&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;-glibc&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">debug!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;indicates glibc and is not compatible with a musl platform&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">continue&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">else&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">asset&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;-musl&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">debug!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;indicates musl&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">else&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">debug!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;name does not indicate the libc it was compiled against&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">libc_matches&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">asset&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">clone&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Pushes even without musl indicator!
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>Impact:&lt;/strong> On musl systems, if there&amp;rsquo;s an asset with no libc indicator and one with &lt;code>-gnu&lt;/code>, the
code filters out &lt;code>-gnu&lt;/code> but accepts the unmarked one, which might actually be glibc-linked.&lt;/p>
&lt;p>&lt;strong>Example:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Platform: x86_64-unknown-linux-musl&lt;/li>
&lt;li>Assets: &lt;code>app-linux-x86_64.tar.gz&lt;/code> (actually glibc), &lt;code>app-linux-x86_64-gnu.tar.gz&lt;/code>&lt;/li>
&lt;li>Result: Selects the unmarked one, which might not work on musl&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Fix:&lt;/strong> When on musl and multiple matches exist, prefer ones explicitly marked &lt;code>-musl&lt;/code>.&lt;/p>
&lt;hr>
&lt;h3 id="8-windows-extension-checking">8. &lt;strong>Windows Extension Checking&lt;/strong>&lt;/h3>
&lt;p>&lt;strong>Location:&lt;/strong> &lt;code>installer.rs&lt;/code>, lines 376-384&lt;/p>
&lt;p>&lt;strong>Issue:&lt;/strong> On Windows, the code assumes files are executable without checking extensions properly:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">is_windows&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="fm">matches!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">entry&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">is_executable&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">with_context&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.})&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">debug!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;found {archive_type} file entry with partial match: `{file_name}`&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">possible_matches&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>Problems:&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>Trusts that partial matches on Windows are executable&lt;/li>
&lt;li>Might select data files like &lt;code>myapp.txt&lt;/code> if they start with the right name&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Fix:&lt;/strong> On Windows, verify the file has &lt;code>.exe&lt;/code> or &lt;code>.bat&lt;/code> extension before accepting partial
matches.&lt;/p>
&lt;hr>
&lt;h3 id="9-sorting-tiebreaker-is-arbitrary">9. &lt;strong>Sorting Tiebreaker is Arbitrary&lt;/strong>&lt;/h3>
&lt;p>&lt;strong>Location:&lt;/strong> &lt;code>picker.rs&lt;/code>, lines 293-302&lt;/p>
&lt;p>&lt;strong>Issue:&lt;/strong> When multiple assets remain after all filtering, the code sorts alphabetically and picks
the first:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">Ok&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">filtered&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">into_iter&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">sorted_by_key&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">clone&lt;/span>&lt;span class="p">())&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">next&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">unwrap&lt;/span>&lt;span class="p">())&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>Problems:&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>Completely arbitrary - depends on naming conventions&lt;/li>
&lt;li>&lt;code>aaa-tool-gnu.tar.gz&lt;/code> beats &lt;code>zzz-tool-musl.tar.gz&lt;/code> alphabetically&lt;/li>
&lt;li>No way to make this deterministic across projects with different naming&lt;/li>
&lt;li>Can lead to selecting the wrong variant (GNU over musl, etc.)&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Impact:&lt;/strong> Unpredictable behavior when multiple variants exist. The selected asset might not be
optimal.&lt;/p>
&lt;p>&lt;strong>Fix:&lt;/strong> Add better heuristics:&lt;/p>
&lt;ul>
&lt;li>Prefer tarballs over zip&lt;/li>
&lt;li>Prefer explicit libc matches over unmarked&lt;/li>
&lt;li>Consider file size (larger = more likely to be feature-complete)&lt;/li>
&lt;li>Make the selection criteria more explicit&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="minor-issues">Minor Issues&lt;/h2>
&lt;h3 id="10-partial-match-case-sensitivity">10. &lt;strong>Partial Match Case Sensitivity&lt;/strong>&lt;/h3>
&lt;p>&lt;strong>Location:&lt;/strong> &lt;code>installer.rs&lt;/code>, lines 410-420&lt;/p>
&lt;p>&lt;strong>Issue:&lt;/strong> The partial match checks case insensitively for the extension but case sensitively for
the prefix:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fn&lt;/span> &lt;span class="nf">archive_member_is_partial_match&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">file_name&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="kt">str&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="kt">bool&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="n">file_name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">starts_with&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">exe_file_stem&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Case sensitive!
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">extensions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">is_empty&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">extensions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">iter&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">any&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">|&amp;amp;&lt;/span>&lt;span class="n">ext&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">file_name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">to_lowercase&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">ends_with&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ext&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Case insensitive!
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>Impact:&lt;/strong> An archive with &lt;code>MyApp&lt;/code> won&amp;rsquo;t match if looking for &lt;code>myapp&lt;/code>, even though the extension
matching is case-insensitive.&lt;/p>
&lt;p>&lt;strong>Fix:&lt;/strong> Make prefix matching case-insensitive too, or document the case-sensitivity requirement.&lt;/p>
&lt;hr>
&lt;h3 id="11-no-validation-of-extension-parsing">11. &lt;strong>No Validation of Extension Parsing&lt;/strong>&lt;/h3>
&lt;p>&lt;strong>Location:&lt;/strong> &lt;code>picker.rs&lt;/code>, lines 121-123&lt;/p>
&lt;p>&lt;strong>Issue:&lt;/strong> Extension parsing errors are silently ignored:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">match&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Extension&lt;/span>::&lt;span class="n">from_path&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Path&lt;/span>::&lt;span class="n">new&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">)).&lt;/span>&lt;span class="n">with_context&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.})&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nb">Err&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">debug!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;skipping asset with invalid extension: {e}&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Just skips silently
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>Impact:&lt;/strong> If there&amp;rsquo;s a bug in extension parsing, valid assets might be silently skipped without
clear errors to the user.&lt;/p>
&lt;p>&lt;strong>Fix:&lt;/strong> Accumulate skipped assets and report them in debug mode or if no assets match.&lt;/p>
&lt;hr>
&lt;h3 id="12---matching-only-applied-when-multiple-matches">12. &lt;strong>&lt;code>--matching&lt;/code> Only Applied When Multiple Matches&lt;/strong>&lt;/h3>
&lt;p>&lt;strong>Location:&lt;/strong> &lt;code>picker.rs&lt;/code>, lines 338-355&lt;/p>
&lt;p>&lt;strong>Issue:&lt;/strong> The &lt;code>--matching&lt;/code> filter is only applied when &lt;code>matches.len() &amp;gt; 1&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fn&lt;/span> &lt;span class="nf">maybe_filter_for_matching_string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">matches&lt;/span>: &lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Asset&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="nb">Result&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Asset&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">matching&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">is_none&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Ok&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">matches&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// This is called from pick_asset_from_matches which is only called after filtering
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// So if there&amp;#39;s only 1 match, this never gets called!
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Actually looking at the code flow in &lt;code>pick_asset_from_matches&lt;/code> (line 270), this is called even with
1 match, but the documentation says it&amp;rsquo;s ignored with one match.&lt;/p>
&lt;p>&lt;strong>Impact:&lt;/strong> User confusion - they set &lt;code>--matching&lt;/code> but it might not have the effect they expect if
there&amp;rsquo;s only one platform match.&lt;/p>
&lt;p>&lt;strong>Fix:&lt;/strong> Document this behavior more clearly or apply matching earlier in the pipeline.&lt;/p>
&lt;hr>
&lt;h2 id="edge-cases--robustness-issues">Edge Cases &amp;amp; Robustness Issues&lt;/h2>
&lt;h3 id="13-no-fallback-when-64-bit-filter-removes-all-assets">13. &lt;strong>No Fallback When 64-bit Filter Removes All Assets&lt;/strong>&lt;/h3>
&lt;p>&lt;strong>Location:&lt;/strong> &lt;code>picker.rs&lt;/code>, lines 322-335&lt;/p>
&lt;p>&lt;strong>Issue:&lt;/strong> If all assets contain &amp;ldquo;64&amp;rdquo; but none are actually 64-bit, they all pass through. But if
some contain &amp;ldquo;64&amp;rdquo; and some don&amp;rsquo;t, only &amp;ldquo;64&amp;rdquo; ones are kept.&lt;/p>
&lt;p>&lt;strong>Example:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Platform: x86_64&lt;/li>
&lt;li>Assets: &lt;code>app-v1.64-i686.tar.gz&lt;/code>, &lt;code>app-v1.64-x86_64.tar.gz&lt;/code>&lt;/li>
&lt;li>Both contain &amp;ldquo;64&amp;rdquo;, both pass through&lt;/li>
&lt;li>Then alphabetical sort picks one&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Impact:&lt;/strong> Can still select wrong architecture even after &amp;ldquo;64-bit filtering.&amp;rdquo;&lt;/p>
&lt;hr>
&lt;h3 id="14-zip-and-7z-archives-dont-track-executability">14. &lt;strong>Zip and 7z Archives Don&amp;rsquo;t Track Executability&lt;/strong>&lt;/h3>
&lt;p>&lt;strong>Location:&lt;/strong> &lt;code>archive.rs&lt;/code>, lines 100-103, 160-163&lt;/p>
&lt;p>&lt;strong>Issue:&lt;/strong> These archive formats don&amp;rsquo;t preserve Unix executable bits:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fn&lt;/span> &lt;span class="nf">is_executable&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="nb">Result&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="kt">bool&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Zip entries do not mark whether something is executable.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Ok&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>Impact:&lt;/strong> On Unix, partial matches from zip files are always accepted (because &lt;code>None&lt;/code> is treated
as executable), even if the file isn&amp;rsquo;t actually executable in the archive.&lt;/p>
&lt;p>&lt;strong>Fix:&lt;/strong> On Unix, after extracting from zip/7z, verify the file is executable or try to make it
executable.&lt;/p>
&lt;hr>
&lt;h3 id="15-race-condition-in-tarball-iteration">15. &lt;strong>Race Condition in Tarball Iteration&lt;/strong>&lt;/h3>
&lt;p>&lt;strong>Location:&lt;/strong> &lt;code>installer.rs&lt;/code>, lines 129-138&lt;/p>
&lt;p>&lt;strong>Issue:&lt;/strong> The code iterates through the tarball twice - once to find the best match, then again to
extract it:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// First iteration to find index
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">mut&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">arch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">tar_reader_for&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">downloaded_file&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">entries&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">arch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">entries&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">idx&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">best_match_from_archive&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// Second iteration to extract
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">mut&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">arch2&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">tar_reader_for&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">downloaded_file&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">for&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">entry&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">in&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">arch2&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">entries&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>Impact:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>If the file is modified between iterations (unlikely but possible)&lt;/li>
&lt;li>If the archive is malformed, the second iteration might behave differently&lt;/li>
&lt;li>Performance cost of reading the archive twice&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Fix:&lt;/strong> This is noted in comments as necessary due to API limitations. Could cache the path and
then use tar&amp;rsquo;s by-name extraction if available.&lt;/p>
&lt;hr>
&lt;h2 id="recommendations">Recommendations&lt;/h2>
&lt;h3 id="high-priority-fixes">High Priority Fixes&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>Fix 64-bit filtering logic&lt;/strong> - Use architecture-specific regex matching instead of naive string
search for &amp;ldquo;64&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>Add executable check for exact matches&lt;/strong> - Critical for security and correctness&lt;/li>
&lt;li>&lt;strong>Improve ARM regex specificity&lt;/strong> - Prevent matching 32-bit ARM variants&lt;/li>
&lt;li>&lt;strong>Fix Android detection&lt;/strong> - Use word boundaries&lt;/li>
&lt;/ol>
&lt;h3 id="medium-priority">Medium Priority&lt;/h3>
&lt;ol start="5">
&lt;li>&lt;strong>Better musl detection&lt;/strong> - Add fallbacks and try multiple binaries&lt;/li>
&lt;li>&lt;strong>Smarter tiebreaker&lt;/strong> - Use better heuristics than alphabetical sort&lt;/li>
&lt;li>&lt;strong>MIPS regex fix&lt;/strong> - Prevent mips_re from matching mips64&lt;/li>
&lt;/ol>
&lt;h3 id="low-priority">Low Priority&lt;/h3>
&lt;ol start="8">
&lt;li>&lt;strong>Case-insensitive partial matching&lt;/strong> - More forgiving for users&lt;/li>
&lt;li>&lt;strong>Better error messages&lt;/strong> - Show what was skipped and why&lt;/li>
&lt;li>&lt;strong>Document limitations&lt;/strong> - Especially around zip/7z executability&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="testing-recommendations">Testing Recommendations&lt;/h2>
&lt;h3 id="unit-tests-needed">Unit Tests Needed&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>Test 64-bit filter with version numbers&lt;/strong> - &lt;code>myapp-v1.64.0-linux-i686.tar.gz&lt;/code> should NOT be
selected on x86_64&lt;/li>
&lt;li>&lt;strong>Test 64-bit filter with correct indicators&lt;/strong> - &lt;code>myapp-linux-x86_64.tar.gz&lt;/code> SHOULD be selected
on x86_64&lt;/li>
&lt;li>Test Android filtering with various Linux ARM64 asset names&lt;/li>
&lt;li>Test exact match with non-executable file&lt;/li>
&lt;li>Test ARM regex with armv7, armhf variants&lt;/li>
&lt;li>Test musl filtering with unmarked binaries&lt;/li>
&lt;li>Test partial match case sensitivity&lt;/li>
&lt;/ol>
&lt;h3 id="integration-tests-needed">Integration Tests Needed&lt;/h3>
&lt;ol>
&lt;li>Test full asset selection with real-world release patterns&lt;/li>
&lt;li>Test with archives containing multiple executables&lt;/li>
&lt;li>Test cross-platform with various naming conventions&lt;/li>
&lt;li>Test error cases (no matching assets, ambiguous matches)&lt;/li>
&lt;/ol>
&lt;h3 id="property-tests">Property Tests&lt;/h3>
&lt;ol>
&lt;li>Asset selection should be deterministic given same inputs&lt;/li>
&lt;li>Selected asset should always be compatible with platform&lt;/li>
&lt;li>Filtering should monotonically decrease candidate set&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>The asset selection logic in ubi is generally well-structured but has several bugs that can lead to
incorrect asset selection, particularly around:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>64-bit architecture detection&lt;/strong> (using naive string search instead of architecture-aware
matching)&lt;/li>
&lt;li>Architecture matching for ARM variants and Android/Linux disambiguation&lt;/li>
&lt;li>Executable detection in archives (exact matches and zip/7z formats)&lt;/li>
&lt;li>Tiebreaking when multiple valid options exist&lt;/li>
&lt;/ul>
&lt;p>The most critical fix needed is improving the 64-bit filtering logic to use architecture-specific
regex patterns instead of a simple substring search for &amp;ldquo;64&amp;rdquo;, which can match version numbers and
other non-architecture contexts.&lt;/p>
&lt;p>Most of these issues can be fixed with more precise regex patterns and better filtering logic. The
codebase is well-tested but would benefit from additional tests covering the edge cases identified
here, particularly around version numbers interfering with architecture detection.&lt;/p>
&lt;/details>
&lt;p>So how well did it do?&lt;/p>
&lt;p>Well, on the plus side, it did spot a few real bugs. On the minus side, it was wrong a lot. Let&amp;rsquo;s go
through each bug in detail.&lt;/p>
&lt;h2 id="1-android-detection-too-broad-linux-arm64-confusion-1">1. &lt;strong>Android Detection Too Broad, Linux ARM64 Confusion&lt;/strong>&lt;/h2>
&lt;p>Yes, this is a real bug. The OS matching regex of &lt;code>(?i:android)&lt;/code>, without word boundaries, is not
good. It would matching things like &amp;ldquo;mandroid&amp;rdquo; and &amp;ldquo;bandroid&amp;rdquo;. And in fact there are GitHub projects
with both of those words in their name!&lt;/p>
&lt;p>However, it turns out that &lt;code>ubi&lt;/code> simply wouldn&amp;rsquo;t work &lt;em>at all&lt;/em> on Android, because I never checked
whether it was running on that OS elsewhere in the code.&lt;/p>
&lt;p>But I consider this a hit in terms of bug detection, even though in practice it didn&amp;rsquo;t matter. I
fixed the regex and enabled &lt;code>ubi&lt;/code> to work on Android.&lt;/p>
&lt;h2 id="2-exact-match-doesnt-check-executability">2. &lt;strong>Exact Match Doesn&amp;rsquo;t Check Executability&lt;/strong>&lt;/h2>
&lt;p>No, this is not a bug. This is in entirely intentional. If &lt;code>ubi&lt;/code> looks through an archive and finds
an exact match for the executable name it expects (e.g. &lt;code>precious&lt;/code> when it downloads from
&lt;a href="https://github.com/houseabsolute/precious)">https://github.com/houseabsolute/precious)&lt;/a>, then it will extract that.&lt;/p>
&lt;p>Bonus points for calling out that &amp;ldquo;[a] malicious or poorly structured archive could have a
non-executable data file with the right name that gets selected over an actual executable&amp;rdquo;. LOL. If
you are dealing with a malicious project, executable name matching is the least of your problems.
You can use &lt;code>ubi&lt;/code> to download malware quite easily!&lt;/p>
&lt;p>But you can also use &lt;em>any&lt;/em> tool that downloads tarballs to download malware.&lt;/p>
&lt;h2 id="3-arm-regex-too-permissive-on-macos-1">3. &lt;strong>ARM Regex Too Permissive on macOS&lt;/strong>&lt;/h2>
&lt;p>No, this is not a bug. Claude is assuming that when people create releases for their projects, they
name them very carefully. But trust me, they absolutely do not. People choose all sorts of nonsense
for naming. A release like &lt;code>some-project-Darwin-arm.tar.gz&lt;/code> is fairly sane compared to some of the
things people do. The code &lt;em>intentionally&lt;/em> matches &lt;code>arm&lt;/code> without a &lt;code>64&lt;/code> here.&lt;/p>
&lt;p>There&amp;rsquo;s literally a comment in the code about doing this.&lt;/p>
&lt;h2 id="4-64-bit-filter-uses-naive-string-search">4. &lt;strong>64-bit Filter Uses Naive String Search&lt;/strong>&lt;/h2>
&lt;p>Yes, this is a bug! I fixed the 64-bit matching to use a regex that looks for CPU names instead of
just the string &lt;code>64&lt;/code>.&lt;/p>
&lt;h2 id="5-mips-regex-ordering-issue-1">5. &lt;strong>MIPS Regex Ordering Issue&lt;/strong>&lt;/h2>
&lt;p>No, not a bug. The code does not use &lt;em>both&lt;/em> the &lt;code>mips_re&lt;/code> and &lt;code>mip64_re&lt;/code> regexes on one system. Ubi
looks at what CPU you are running on and picks one of these regexes based on that.&lt;/p>
&lt;h2 id="6-musl-detection-relies-on-external-binary-1">6. &lt;strong>Musl Detection Relies on External Binary&lt;/strong>&lt;/h2>
&lt;p>No? I mean, this musl detection code is really dumb. But I don&amp;rsquo;t know of a better way to do this.&lt;/p>
&lt;h2 id="7-libc-filtering-is-too-lenient-1">7. &lt;strong>Libc Filtering is Too Lenient&lt;/strong>&lt;/h2>
&lt;p>Not a bug. In this case, &lt;code>ubi&lt;/code> fails by downloading the wrong binary. If I made the filtering
stricter, it would sometimes fail by &lt;em>not&lt;/em> downloading a binary that could work. People use &lt;code>ubi&lt;/code> to
download tools written in languages that don&amp;rsquo;t link to the system&amp;rsquo;s libc, like shell scripts,
Python, Go binaries, etc.&lt;/p>
&lt;h2 id="8-windows-extension-checking-1">8. &lt;strong>Windows Extension Checking&lt;/strong>&lt;/h2>
&lt;p>This is the same issue as #2 above, but on Windows, it&amp;rsquo;s even more important to not check
executability, because zip files, which are very common for Windows releases, don&amp;rsquo;t even include
that information!&lt;/p>
&lt;h2 id="9-sorting-tiebreaker-is-arbitrary-1">9. &lt;strong>Sorting Tiebreaker is Arbitrary&lt;/strong>&lt;/h2>
&lt;p>Maybe? The suggestion to prefer an asset with a libc in the name over one without isn&amp;rsquo;t bad. I
wouldn&amp;rsquo;t call this a &lt;em>bug&lt;/em>, since it works as designed, but this could be improved. That said, I&amp;rsquo;m
not sure how often this matters. I haven&amp;rsquo;t had any bug reports where changing this behavior would
improve &lt;code>ubi&lt;/code> for someone.&lt;/p>
&lt;h2 id="10-partial-match-case-sensitivity-1">10. &lt;strong>Partial Match Case Sensitivity&lt;/strong>&lt;/h2>
&lt;p>Yeah, this is already called out in &lt;a href="https://github.com/houseabsolute/ubi/issues/83">issue #83&lt;/a>.&lt;/p>
&lt;h2 id="11-no-validation-of-extension-parsing-1">11. &lt;strong>No Validation of Extension Parsing&lt;/strong>&lt;/h2>
&lt;p>Eh, I guess. Maybe? This would be a lot of work for very little gain. When the extension parsing is
broken, I usually get a bug report ;)&lt;/p>
&lt;h2 id="12---matching-only-applied-when-multiple-matches-1">12. &lt;strong>&lt;code>--matching&lt;/code> Only Applied When Multiple Matches&lt;/strong>&lt;/h2>
&lt;p>No, this is not a bug. This is literally exactly what the docs say it will do!&lt;/p>
&lt;p>Also, it&amp;rsquo;s &lt;em>wrong&lt;/em> in saying &amp;ldquo;this is called even with 1 match&amp;rdquo;. I don&amp;rsquo;t understand how it concluded
this.&lt;/p>
&lt;h2 id="13-no-fallback-when-64-bit-filter-removes-all-assets-1">13. &lt;strong>No Fallback When 64-bit Filter Removes All Assets&lt;/strong>&lt;/h2>
&lt;p>This is a bit weird. The title doesn&amp;rsquo;t match the bug description. But the title is wrong. Ubi &lt;em>does&lt;/em>
have a fallback if the 64-bit filter removes everything.&lt;/p>
&lt;p>Then the actual bug description seems to be about issue 4, that the 64-bit filter is too permissive.&lt;/p>
&lt;h2 id="14-zip-and-7z-archives-dont-track-executability-1">14. &lt;strong>Zip and 7z Archives Don&amp;rsquo;t Track Executability&lt;/strong>&lt;/h2>
&lt;p>Not a bug. The behavior of &lt;code>ubi&lt;/code> here is intentional, and Claude&amp;rsquo;s suggestion of &amp;ldquo;after extracting
from zip/7z, verify the file is executable or try to make it executable&amp;rdquo; is nonsense. A file
extracted from a zip/7z is never going to be executable! And &lt;code>ubi&lt;/code> already makes it executable (and
will do this with any archive file type).&lt;/p>
&lt;h2 id="15-race-condition-in-tarball-iteration-1">15. &lt;strong>Race Condition in Tarball Iteration&lt;/strong>&lt;/h2>
&lt;p>Yes, but no. Nothing is going to modify the file while &lt;code>ubi&lt;/code> is working with it. I &lt;em>guess&lt;/em> it could
in theory be deleted by some sort of temp reaper, but whatever.&lt;/p>
&lt;h2 id="in-summary">In Summary&lt;/h2>
&lt;p>Out of 15 issues reported, here&amp;rsquo;s my evaluation:&lt;/p>
&lt;ul>
&lt;li>It found 1 real and important bug, the 64-bit filtering issue, issue 4.&lt;/li>
&lt;li>It found 1 real bug that was irrelevant, the Android matching issue, issue 1. It did not notice
that this particular code could never be executed.&lt;/li>
&lt;li>It asserted a bunch of things were bugs that are intentional behavior, issues 2, 3, 5, 7, 8, 12,
13, and 14.&lt;/li>
&lt;li>It found 1 thing I&amp;rsquo;d already noted in &lt;code>ubi&lt;/code>&amp;rsquo;s issues, that executable matching should probably be
case-insensitive, issue 10.&lt;/li>
&lt;li>It found some things that can be improved, but aren&amp;rsquo;t very important, issues 6, 9, 11, and 15.&lt;/li>
&lt;/ul>
&lt;p>It&amp;rsquo;s great that it found some legit bugs, and I&amp;rsquo;m glad to fix them. But this sure is a lot of noise.
For many of these, it seems like maybe it wasn&amp;rsquo;t considering enough of the context (other code,
docs, etc.) when reporting something as a bug.&lt;/p>
&lt;p>Would I do this again with other code bases? Yes, I think so. I didn&amp;rsquo;t burn a ton of time on dealing
with the false positives, and I am happy it found at least one real bug that no one had reported
yet.&lt;/p></description></item><item><title>My Team at MongoDB Is Hiring a Lead Engineer</title><link>https://blog.urth.org/2025/05/23/my-team-at-mongodb-is-hiring-a-lead-engineer/</link><pubDate>Fri, 23 May 2025 11:20:29 -0500</pubDate><guid>https://blog.urth.org/2025/05/23/my-team-at-mongodb-is-hiring-a-lead-engineer/</guid><description>&lt;p>Have you ever tried to tell me what to do, only to find I ignored you? Well, now you can tell me
what to do and I&amp;rsquo;ll &lt;em>have&lt;/em> to listen to you! Want me to drop and do 50 push-ups? Sure! Shine your
shoes? Done! Be your number one Yes Man? Yes, Boss!&lt;/p>
&lt;p>MongoDB is hiring a Lead Engineer to lead the Verification and Correctness team within Cluster to
Cluster Synchronization. You will be leading and coaching a team of engineers who are working to
make migration of MongoDB clusters easy, fast, and reliable for our customers, as well as work with
product managers and program managers to specify, prioritize and deliver new features that delight
our users.&lt;/p>
&lt;p>&lt;strong>This role can be based out of our New York City office or remotely in North America.&lt;/strong>&lt;/p>
&lt;p>That&amp;rsquo;s my team, so your dreams of making me your servant can finally come true&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>!
&lt;a href="https://grnh.se/0ef999ff1us">Apply today&lt;/a>!&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Promises of servitude may not be fulfilled exactly as described. You apply to this position at
your own risk. There is no warranty for this application.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>My Azure Pipelines Tooling Is Dead</title><link>https://blog.urth.org/2025/04/02/my-azure-pipelines-tooling-is-dead/</link><pubDate>Wed, 02 Apr 2025 16:56:02 +0200</pubDate><guid>https://blog.urth.org/2025/04/02/my-azure-pipelines-tooling-is-dead/</guid><description>&lt;p>Many years ago, in the glory days of 2019, when I was young and carefree, with the wind in my hair
and all the youthful joy of the world in my soul, I
&lt;a href="https://blog.urth.org/2019/11/18/my-new-ci-helpers-for-perl/">created my &lt;code>ci-perl-helpers&lt;/code> project for testing Perl modules in Azure Pipelines&lt;/a>.
Why Azure Pipelines? Well, at the time, GitHub Actions didn&amp;rsquo;t support caching, and it was generally
immature.&lt;/p>
&lt;p>How things have changed. GitHub Actions is the de facto default for FOSS project these days. It
supports caching, and is otherwise fairly mature. Meanwhile, Azure Pipelines appears to be mostly
neglected.&lt;/p>
&lt;p>Recently, something changed in Azure Pipelines that broke my
&lt;a href="https://github.com/houseabsolute/ci-perl-helpers">&lt;code>ci-perl-helpers&lt;/code> project&lt;/a>. I&amp;rsquo;m not sure &lt;em>what&lt;/em>
changed, but I don&amp;rsquo;t have the interest or energy to investigate it. This project did a lot of cool
stuff, but it was a pile of confusing shell scripts and generated Dockerfiles and stuff. Also, from
what I can tell, no one but me ever used it.&lt;/p>
&lt;p>I&amp;rsquo;ve started to move my Perl modules over to GitHub Actions using a much simpler workflow. This
workflow builds on top of
&lt;a href="https://github.com/shogo82148/actions-setup-perl">&lt;code>shogo82148/actions-setup-perl&lt;/code>&lt;/a>, which installs
a pre-compiled Perl. Along with GitHub caching for prereqs, this makes running CI in GitHub Actions
performant enough to not need all the complexity of my &lt;code>ci-perl-helpers&lt;/code> project.&lt;/p>
&lt;p>I might take
&lt;a href="https://github.com/houseabsolute/DateTime-Locale/blob/master/.github/workflows/ci.yml">what I&amp;rsquo;ve done for DateTime-Locale&lt;/a>
and turn it into a real action. But that&amp;rsquo;s work, so I might just make it something &lt;em>I&lt;/em> can re-use
easily, without making it a real Action.&lt;/p>
&lt;p>The downside to this is I&amp;rsquo;m no longer able to test with Perl dev releases or blead (git &lt;code>HEAD&lt;/code>). The
Perl that &lt;a href="https://github.com/shogo82148/actions-setup-perl">&lt;code>shogo82148/actions-setup-perl&lt;/code>&lt;/a>
installs comes from the
&lt;a href="https://github.com/skaji/relocatable-perl">&lt;code>skaji/relocatable-perl&lt;/code> project&lt;/a>, and that project
doesn&amp;rsquo;t provide dev versions of Perl.&lt;/p>
&lt;p>I&amp;rsquo;m also going to just cut down my testing breadth a fair bit. With my Azure Pipelines setup, I&amp;rsquo;d
test on every stable Perl from 5.8.9 or 5.10.1 to the latest Perl, plus dev and blead. With my new
setup, I plan to test one old version (5.10.1 for most modules), plus the latest two versions of
Perl. I&amp;rsquo;ll also test with &lt;em>only&lt;/em> the latest Perl on macOS and Windows. I think this should be enough
to keep my modules stable on the Perl versions people care about.&lt;/p></description></item><item><title>Should People Just Use Goreleaser Instead of `actions-rust-release`?</title><link>https://blog.urth.org/2025/02/16/should-people-just-use-goreleaser-instead-of-actions-rust-release/</link><pubDate>Sun, 16 Feb 2025 16:25:57 -0600</pubDate><guid>https://blog.urth.org/2025/02/16/should-people-just-use-goreleaser-instead-of-actions-rust-release/</guid><description>&lt;p>I&amp;rsquo;m cross-posting this from
&lt;a href="https://github.com/houseabsolute/actions-rust-release/issues/10">an issue I made&lt;/a> for
&lt;a href="https://github.com/houseabsolute/actions-rust-release">&lt;code>actions-rust-release&lt;/code>&lt;/a>. For context, &lt;em>I&lt;/em> am
the action&amp;rsquo;s author, and this is a serious question, not the start of a pitch for why you should use
my action.&lt;/p>
&lt;p>TLDR; Does &lt;code>actions-rust-release&lt;/code> serve any purpose that isn&amp;rsquo;t better served by
&lt;a href="https://goreleaser.com/">goreleaser&lt;/a>?&lt;/p>
&lt;p>Here&amp;rsquo;s the issue body in full:&lt;/p>
&lt;blockquote>
&lt;p>Recently, I was considering adding some features to this action, notably adding the ability to
produce signed releases (specifically, signing the checksums file). As I started looking into
this, I realized that &lt;a href="https://goreleaser.com/">goreleaser&lt;/a> already does this, as well as many
other things this action doesn&amp;rsquo;t do:&lt;/p>
&lt;ul>
&lt;li>It offers a lot more power and flexibility in what is included in the resulting release archive
files.
&lt;ul>
&lt;li>This includes templating files, so for example you can update the copyright year in the
&lt;code>LICENSE&lt;/code> file to match the release date.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Deb, RPM, macOS DMG, MSI, Chocolatey, etc. support.&lt;/li>
&lt;li>Integration with SBOM creation tools.&lt;/li>
&lt;li>And more&lt;/li>
&lt;/ul>
&lt;p>So I&amp;rsquo;m wondering whether it&amp;rsquo;s worth continuing to invest in this action. It seems like using
goreleaser to release a Rust project is fairly easy. It even supports &lt;em>building&lt;/em>, though I think
for that I&amp;rsquo;d still use my
&lt;a href="https://github.com/houseabsolute/actions-rust-cross">actions-rust-cross&lt;/a> action, as I don&amp;rsquo;t think
goreleaser would make it easier to do cross-platform builds.&lt;/p>
&lt;p>Will people who use this action see this issue? If you do, I&amp;rsquo;d greatly appreciate your feedback!
Take a look at &lt;a href="https://goreleaser.com/">goreleaser&lt;/a>, focusing specifically on the parts related
to releasing, not building. After looking, do you still prefer this actions? If so, why?&lt;/p>
&lt;/blockquote>
&lt;p>Please
&lt;a href="https://github.com/houseabsolute/actions-rust-release/issues/10">provide your feedback on that issue&lt;/a>.&lt;/p></description></item><item><title>My actions-rust-cross Action Now Has Built-In Caching</title><link>https://blog.urth.org/2024/12/21/my-actions-rust-cross-action-now-has-built-in-caching/</link><pubDate>Sat, 21 Dec 2024 22:20:50 -0600</pubDate><guid>https://blog.urth.org/2024/12/21/my-actions-rust-cross-action-now-has-built-in-caching/</guid><description>&lt;p>I just released v1.0.0 Beta 1 of
&lt;a href="https://github.com/houseabsolute/actions-rust-cross/tree/v1.0.0-beta1">my &lt;code>actions-rust-cross&lt;/code> GitHub Action&lt;/a>.
The big headline feature in this release is integration with the
&lt;a href="https://github.com/Swatinem/rust-cache">&lt;code>Swatinem/rust-cache&lt;/code>&lt;/a> action. This will include the
&lt;code>target&lt;/code> you provide to &lt;code>actions-rust-cross&lt;/code> as part of the cache key, which from my testing means
that it only caches the compilation output relevant to the target platform.&lt;/p>
&lt;p>The &lt;code>Swatinem/rust-cache&lt;/code> action caches compiled &lt;em>dependencies&lt;/em>, so how helpful this will be for
your project depends on how much time it spends compiling dependencies versus the project itself.&lt;/p>
&lt;p>From some informal testing with &lt;a href="https://github.com/houseabsolute/ubi">my &lt;code>ubi&lt;/code> project&lt;/a>, I saw that
caching reduced build and test time for 21 different targets from 9.5 minutes to around 4.5 minutes.
That project has about 255 crate dependencies when I run &lt;code>cargo test&lt;/code>.&lt;/p>
&lt;p>Note that according to the docs for &lt;code>Swatinem/rust-cache&lt;/code>, this caching is most useful for projects
with a &lt;code>Cargo.lock&lt;/code> file in their repo. For libraries, this caching is less helpful, since &lt;code>cargo&lt;/code>
is always pulling new dependency versions as they&amp;rsquo;re released. However, I suspect that for runs in
quick succession this caching might still work well. You can always disable caching entirely by
setting &lt;code>use-rust-cache&lt;/code> to &lt;code>false&lt;/code> when invoking &lt;code>actions-rust-cross&lt;/code>.&lt;/p>
&lt;p>You can also configure the &lt;code>Swatinem/rust-cache&lt;/code> action by passing a JSON string in the
&lt;code>rust-cache-parameters&lt;/code> parameter to &lt;code>actions-rust-cross&lt;/code>. This is a bit awkward, but it was the
best way I could figure out how to take parameters &lt;em>without&lt;/em> copying all of the &lt;code>rust-cache&lt;/code>
parameters to &lt;code>actions-rust-cross&lt;/code>.&lt;/p>
&lt;p>In order to take advantage of this release, you just need to change your &lt;code>uses&lt;/code> to
&lt;code>houseabsolute/actions-rust-cross@v1&lt;/code>. If you&amp;rsquo;re already calling &lt;code>Swatinem/rust-cache&lt;/code> separately
from &lt;code>actions-rust-cross&lt;/code>, you should delete that from your workflow.&lt;/p>
&lt;p>I&amp;rsquo;d love to get some feedback on this release before marking it stable. If you try it and it doesn&amp;rsquo;t
work, &lt;a href="https://github.com/houseabsolute/actions-rust-cross/issues">please file a GitHub issue&lt;/a>. If
it &lt;em>does&lt;/em> work for you, &lt;a href="mailto:autarch@urth.org">please email me and let me know&lt;/a>!&lt;/p></description></item><item><title>My New GitHub Action for Releasing Rust Projects</title><link>https://blog.urth.org/2024/10/27/my-new-github-action-for-releasing-rust-projects/</link><pubDate>Sun, 27 Oct 2024 15:49:39 -0500</pubDate><guid>https://blog.urth.org/2024/10/27/my-new-github-action-for-releasing-rust-projects/</guid><description>&lt;p>A while back I created a
&lt;a href="https://github.com/marketplace/actions/release-rust-project-binaries-as-github-releases">new GitHub Action for releasing Rust projects&lt;/a>
which produce binary releases (as opposed to crates). This works nicely with my
&lt;a href="https://github.com/marketplace/actions/build-rust-projects-with-cross">Rust Cross Action&lt;/a> to let me
automate away most of the toil from releasing new versions of my Rust CLI tools like
&lt;a href="https://github.com/houseabsolute/precious">&lt;code>precious&lt;/code>&lt;/a>,
&lt;a href="https://github.com/houseabsolute/ubi">&lt;code>ubi&lt;/code>&lt;/a>, and
&lt;a href="https://github.com/houseabsolute/omegasort">&lt;code>omegasort&lt;/code>&lt;/a>.&lt;/p>
&lt;p>Using it is pretty simple and can be done in one step:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Publish artifacts and release&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">houseabsolute/actions-rust-release@v0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">executable-name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x86_64-unknown-linux-musl&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>For a longer example, see how I use this action and my Rust Cross Action to
&lt;a href="https://github.com/houseabsolute/ubi/blob/master/.github/workflows/ci.yml">build, test, and release &lt;code>ubi&lt;/code>&lt;/a>.&lt;/p>
&lt;p>Right now, all it does is the following:&lt;/p>
&lt;ul>
&lt;li>Creates a tarball (most platforms) or zip file (Windows) with the executable, along with any extra
files that are requested. By default, it will include a changes file (defaults to &lt;code>Changes.md&lt;/code>)
and anything matching &lt;code>README*&lt;/code>.&lt;/li>
&lt;li>Creates a sha256 checksum file for this archive file.&lt;/li>
&lt;li>Uploads these two files as release artifacts for the workflow run in GitHub.&lt;/li>
&lt;li>Uses the &lt;a href="https://github.com/marketplace/actions/gh-release">GH Release Action&lt;/a> to do the actual
release publication on GitHub.&lt;/li>
&lt;/ul>
&lt;p>It&amp;rsquo;s not much, but I was copying the YAML config to do this between my Rust projects, so it made
sense to turn this into its own standalone Action.&lt;/p>
&lt;p>In the future, I might turn this into something a little more generic. The only Rust-specific piece
of it is that it knows where to &lt;code>cargo build&lt;/code> puts the executable file. But there&amp;rsquo;s no reason this
couldn&amp;rsquo;t work just as well for any other language capable of producing single-file binaries, like
Go.&lt;/p></description></item><item><title>My Team at MongoDB Has 3 Senior Dev Openings</title><link>https://blog.urth.org/2024/07/16/my-team-at-mongodb-has-3-senior-dev-openings/</link><pubDate>Tue, 16 Jul 2024 13:14:46 -0500</pubDate><guid>https://blog.urth.org/2024/07/16/my-team-at-mongodb-has-3-senior-dev-openings/</guid><description>&lt;p>My team at MongoDB is &lt;strong>three&lt;/strong> Senior Engineers. For these positions you can be 100% remote in the
US or Canada or you can choose to work from one our offices. Our schedule is based around our NYC
office on the east coast, so if you&amp;rsquo;re on the west coast your hours would need to account for that.&lt;/p>
&lt;p>My team primarily works on
&lt;a href="https://www.mongodb.com/docs/cluster-to-cluster-sync/current/">MongoDB&amp;rsquo;s Cluster-to-Cluster Sync product&lt;/a>.
It&amp;rsquo;s an interesting and challenging product to work on. As a big bonus, this is &lt;em>not&lt;/em> a live a
service and there&amp;rsquo;s no on-call. We do actual product releases, which still feels like a novelty to
me after so many years doing web apps and services.&lt;/p>
&lt;p>There are two different postings for the US and Canada:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://grnh.se/1e9e68151us">United States&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://grnh.se/1b7bdf8e1us">Canada&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>(Note that these are referral links so I get credit for referring you if you use them.)&lt;/p>
&lt;p>I&amp;rsquo;ve been at MongoDB since May of this 2022 and it&amp;rsquo;s been great. If you have questions about the
position, the team, or working at MongoDB, &lt;a href="mailto:autarch@urth.org">please reach out&lt;/a>.&lt;/p></description></item><item><title>Short Reviews of the Anime I've Watched in Taiwan</title><link>https://blog.urth.org/2024/06/10/short-reviews-of-the-anime-i-ve-watched-in-taiwan/</link><pubDate>Mon, 10 Jun 2024 15:03:54 +0800</pubDate><guid>https://blog.urth.org/2024/06/10/short-reviews-of-the-anime-i-ve-watched-in-taiwan/</guid><description>&lt;p>I&amp;rsquo;ve been living in Taiwan since December 20 of last year and I&amp;rsquo;ll return on June 29. There&amp;rsquo;s a lot
of things I enjoy about living here, and one of them is that the content on Netflix here is quite
different. In particular, there&amp;rsquo;s a lot of anime available that&amp;rsquo;s only available through
&lt;a href="https://www.crunchyroll.com/">Crunchyroll&lt;/a>&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> in the US.&lt;/p>
&lt;p>I also have had more free time here, so I&amp;rsquo;ve ended up watching a lot of anime, both with my wife and
by myself.&lt;/p>
&lt;p>So here&amp;rsquo;s some quick reviews of most of them, with a ratings on a 1-5 scale:&lt;/p>
&lt;ol>
&lt;li>Terrible&lt;/li>
&lt;li>Could be worse&lt;/li>
&lt;li>Okay - good enough to keep watching at this point&lt;/li>
&lt;li>Great&lt;/li>
&lt;li>Excellent&lt;/li>
&lt;/ol>
&lt;h2 id="blue-eye-samurai">Blue Eye Samurai&lt;/h2>
&lt;p>Technically not an anime, but definitely influenced by them. This is a great show, with an
interesting story and stunning animation. I highly recommend checking this out, but note that it has
a &lt;em>lot&lt;/em> graphic violence and a some not-too-graphic sexual content as well. It&amp;rsquo;s also quite dark and
disturbing, with a very self-hating MC.&lt;/p>
&lt;p>&lt;strong>Rating: 5&lt;/strong>&lt;/p>
&lt;h2 id="the-apothecary-diaries">The Apothecary Diaries&lt;/h2>
&lt;p>This is one of my favorite new animes. It has a smart and funny main character, an interesting
setting (the Imperial Court of China around 1,000 years ago), and really interesting, well
though-out stories. It&amp;rsquo;s a mix of detective show and political drama. All of this makes it very
unlike other anime. I look forward to more seasons.&lt;/p>
&lt;p>&lt;strong>Rating: 5&lt;/strong>&lt;/p>
&lt;h2 id="the-masterful-cat-is-depressed-again-today">The Masterful Cat Is Depressed Again Today&lt;/h2>
&lt;p>A weird slice of life anime with a giant house cat with human-level (or greater) intelligence, who
can do things like housework and cooking, but can&amp;rsquo;t talk. It&amp;rsquo;s pretty silly, but a lot of fun.&lt;/p>
&lt;p>&lt;strong>Rating: 4&lt;/strong>&lt;/p>
&lt;h2 id="skip-and-loafer">Skip and Loafer&lt;/h2>
&lt;p>The main characters do a cute, goofy dance during the opening credits, which is always the sign of a
good anime. This is a very sweet show about some kids in high school and their relationships. It&amp;rsquo;s
also notable for including a trans character and not making this a focus of the show. She&amp;rsquo;s just
there. I haven&amp;rsquo;t really seen that before.&lt;/p>
&lt;p>&lt;strong>Rating : 4&lt;/strong> (I&amp;rsquo;m tempted to say 4.5 but then I might as well use a scale of 1-10, which I don&amp;rsquo;t
want to do, so 4 it is.)&lt;/p>
&lt;h2 id="a-sign-of-affection">A Sign of Affection&lt;/h2>
&lt;p>A romantic story about a deaf college student falling in love. Cute and fun.&lt;/p>
&lt;p>&lt;strong>Rating: 4&lt;/strong>&lt;/p>
&lt;h2 id="my-love-story-with-yamada-kun-at-lv999">My Love Story with Yamada-kun at Lv999&lt;/h2>
&lt;p>A romantic story about a college student (who&amp;rsquo;s not deaf) falling in love with a handsome gamer
nerd. Cute and fun.&lt;/p>
&lt;p>&lt;strong>Rating: 4&lt;/strong>&lt;/p>
&lt;h2 id="parasyte">Parasyte&lt;/h2>
&lt;p>A combination of sci-fi/horror/thriller that&amp;rsquo;s one of the best written anime I&amp;rsquo;ve seen. This show
provokes a lot of thought without having the characters sit around and have annoyingly in-your-face
philsophical conversations. Instead, it makes you think simply by presenting us with interesting
situations and moral choices. It also has fun action scenes, and the music is good, though I wish
they&amp;rsquo;d sprung to pay for more different music. You hear the same snippets a lot. I really loved this
one.&lt;/p>
&lt;p>&lt;strong>Rating: 5&lt;/strong>&lt;/p>
&lt;h2 id="black-clover">Black Clover&lt;/h2>
&lt;p>We didn&amp;rsquo;t get too far into this one (maybe 6 episodes). It seems like a lot of other progression
fantasy type shows. I found the voice acting for one of the main characters incredibly annoying too.
There doesn&amp;rsquo;t seem to be too much different about this show from so many others like it, but which
are better (like Demon Slayer or My Hero Academia).&lt;/p>
&lt;p>&lt;strong>Rating: 2&lt;/strong>&lt;/p>
&lt;h2 id="spice-and-wolf-2024">Spice and Wolf 2024&lt;/h2>
&lt;p>This so far seems to be more or less the same as the original, with the same voice actors and plots.
But if it ain&amp;rsquo;t broke, don&amp;rsquo;t fix it. It&amp;rsquo;s as fun, charming, and interesting as the original so far.
A lot of the story is about economics and trading, which makes it stand out most other fantasy
anime. This one also has excellent animation.&lt;/p>
&lt;p>&lt;strong>Rating: 5&lt;/strong>&lt;/p>
&lt;h2 id="delicious-in-dungeon">Delicious in Dungeon&lt;/h2>
&lt;p>&amp;ldquo;An army marches on its stomach&amp;rdquo;, and so do adventurers in a dungeon. It&amp;rsquo;s basically an RPG-ish
style anime with a lot of cooking scenes. It&amp;rsquo;s a fun take on an old subject, but nothing
ground-breaking.&lt;/p>
&lt;p>&lt;strong>Rating: 3&lt;/strong>&lt;/p>
&lt;h2 id="the-unwanted-undead-adventurer">The Unwanted Undead Adventurer&lt;/h2>
&lt;p>A progression fantasy about a low-level adventurer who dies and is reborn as a skeleton. Step one,
get enough XP to evolve into a form that can talk. A fun take on the progression fantasy genre.&lt;/p>
&lt;p>&lt;strong>Rating: 3&lt;/strong>&lt;/p>
&lt;h2 id="solo-levelling">Solo Levelling&lt;/h2>
&lt;p>A progression fantasy (seeing a theme here?) with an interesting setting, where the dungeons are
accessed via holes in reality from &lt;em>our&lt;/em> world. The main character becomes the first person able to
level up (the others are NPCs?). Fun and a little different from other progression fantasy. It&amp;rsquo;ll be
interesting to see where this goes in future seasons.&lt;/p>
&lt;p>&lt;strong>Rating: 3&lt;/strong>&lt;/p>
&lt;h2 id="shangri-la-frontier">Shangri-La Frontier&lt;/h2>
&lt;p>Do you like watching people play video games? I do. In this show, a guy plays a full-immersion VR
RPG (via some sort of unexplained VR tech that is commonplace in the show&amp;rsquo;s universe). The video
game he plays seems quite cool. It&amp;rsquo;s a MMORPG with &lt;em>way&lt;/em> more depth and story than any existing
game. I&amp;rsquo;d like to play this game.&lt;/p>
&lt;p>But all of the scenes outside the RPG are boring AF. Also, if people really spent so many hours a
day lying in bed with a VR helmet on they&amp;rsquo;d either be morbidly obese or have no muscle whatsoever,
or both. All of the main characters should expect to die of cardiac failure in their 30s or 40s if
they keep this up.&lt;/p>
&lt;p>&lt;strong>Rating: 3&lt;/strong>&lt;/p>
&lt;h2 id="campfire-cooking-in-another-world-with-my-absurd-skill">Campfire Cooking in Another World with My Absurd Skill&lt;/h2>
&lt;p>The first of many isekai&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> anime I&amp;rsquo;ve watched recently. Our MC is summoned to another world as a
hero and gets the powerful skill of &amp;hellip; online shopping. He can order anything he can find from a
very well-stocked online grocery/general store. This is surprisingly powerful and useful. Also,
there&amp;rsquo;s a lot of cooking. So it&amp;rsquo;s sort of Delicious in Dungeon as an isekai story.&lt;/p>
&lt;p>&lt;strong>Rating: 3&lt;/strong>&lt;/p>
&lt;h2 id="mushoku-tensei-jobless-reincarnation">Mushoku Tensei: Jobless Reincarnation&lt;/h2>
&lt;p>&lt;strong>This anime is vile and disgusting. Do not watch it.&lt;/strong> Our MC is either a pedophile or sociopath or
both. I noped out of this show when he gropes a sleeping pre-pubescent girl and comments on her
breasts. The way it&amp;rsquo;s written almost makes it seem like the writers think this is funny. I don&amp;rsquo;t get
it. It&amp;rsquo;s amazing I watched up until that point. Just writing about it makes me want to shower.&lt;/p>
&lt;p>Bizarrely, this is one of the most highly rated isekai shows on MyAnimeList. WTF?!&lt;/p>
&lt;p>&lt;strong>Rating: 1&lt;/strong>&lt;/p>
&lt;h2 id="konosuba-gods-blessing-on-this-wonderful-world">KonoSuba: God&amp;rsquo;s Blessing on This Wonderful World!&lt;/h2>
&lt;p>&lt;em>This&lt;/em> is what a show with a lot of sexual humor should look like. It&amp;rsquo;s freaking hilarious. Our hero
is reborn in a fantasy world and forms an adventuring party with a group of other losers and
weirdos. They&amp;rsquo;re basically all idiots. This show makes fun of a lot of isekai tropes and does it
really well, while telling its own compelling story.&lt;/p>
&lt;p>&lt;strong>Rating: 5&lt;/strong>&lt;/p>
&lt;h2 id="rezero---starting-life-in-another-world">Re:ZERO - Starting Life in Another World&lt;/h2>
&lt;p>One of the best isekai anime I&amp;rsquo;ve watched. In this one, our hero&amp;rsquo;s special power is that when he
dies time rewinds a few days (or so), but his memories are intact. So it&amp;rsquo;s a lot of him screwing
things up and trying over and over again. This show could&amp;rsquo;ve taken this all very lightly, but it&amp;rsquo;s
actually quite emotionally intense. He ends up suffering a lot, both in his own deaths and by seeing
horrible things happen to the people around him. I also appreciated that the MC often acts in a way
that&amp;rsquo;s really unlikeable but very realistic.&lt;/p>
&lt;p>I&amp;rsquo;ve only watched season 1 so far but I&amp;rsquo;m looking forward to the rest.&lt;/p>
&lt;p>&lt;strong>Rating: 5&lt;/strong>&lt;/p>
&lt;h2 id="log-horizon">Log Horizon&lt;/h2>
&lt;p>Wow, so, so boring. A bunch of people are playing a MMORPG only to discover that they&amp;rsquo;re trapped in
it and cannot leave the game. Early on, we establish that they can&amp;rsquo;t die either. Instead of having
&lt;em>any&lt;/em> emotional reaction, they all seem &amp;hellip; fine? Like zero reaction to this insane event. And then
they proceed to do things with goals that make no sense. Like why should I care about what guild
anyone is in? How is this important relative to &amp;ldquo;I&amp;rsquo;m trapped in a fucking videogame, possibly for
eternity&amp;rdquo;?&lt;/p>
&lt;p>I gave up after around 6 episodes.&lt;/p>
&lt;p>&lt;strong>Rating: 2&lt;/strong>&lt;/p>
&lt;h2 id="tsukimichi---moonlit-fantasy">TSUKIMICHI - Moonlit Fantasy&lt;/h2>
&lt;p>A very standard isekai. Hero is summoned to another world, is super powerful, attracts a harem. This
one is entertaining but nothing special. I watched all of it recently but I&amp;rsquo;ve mostly forgotten the
details already.&lt;/p>
&lt;p>&lt;strong>Rating: 3&lt;/strong>&lt;/p>
&lt;h2 id="the-magical-revolution-of-the-reincarnated-princess-and-the-genius-young-lady">The Magical Revolution of the Reincarnated Princess and the Genius Young Lady&lt;/h2>
&lt;p>What an interesting premise. There&amp;rsquo;s a princess who is actually someone from our world reincarnated.
Then she finds ways to use magic to advance technology in this world.&lt;/p>
&lt;p>That&amp;rsquo;s the theory at least. In practice, there&amp;rsquo;s a &lt;em>little&lt;/em> of that, but it takes back seat to lots
of &amp;hellip; nothing. Nothing happens most of the time. Such an interesting premise and so little done
with it. Very boring.&lt;/p>
&lt;p>I gave up after around 7 episodes.&lt;/p>
&lt;p>&lt;strong>Rating: 2&lt;/strong>&lt;/p>
&lt;h2 id="how-a-realist-hero-rebuilt-the-kingdom">How a Realist Hero Rebuilt the Kingdom&lt;/h2>
&lt;p>Another interesting premise. In this one our summoned hero is not powerful, but he knows a lot about
economics and governance. Also, he&amp;rsquo;s read Machiavelli&amp;rsquo;s &lt;em>The Prince&lt;/em>. This one is a bit uneven, but
interesting. I think it could&amp;rsquo;ve leaned a lot harder into the realpolitik approach and showed more
of the political maneuvering.&lt;/p>
&lt;p>Also, a lot of what he does is a bit silly, and the story doesn&amp;rsquo;t really take its own premise that
seriously. Like when the show starts there&amp;rsquo;s supposed to be a major food shortage in the kingdom,
but there are no riots, protests, fights, etc. And to help combat the food shortage he introduced
people to eating &amp;hellip; octopus. Because starving people need to be told to try eating some animal
that&amp;rsquo;s been around forever, obviously. Maybe &amp;ldquo;starving&amp;rdquo; was a mistranslation and in fact the people
were just slightly peckish.&lt;/p>
&lt;p>&lt;strong>Rating: 3&lt;/strong>&lt;/p>
&lt;h2 id="that-time-i-got-reincarnated-as-a-slime">That Time I Got Reincarnated as a Slime&lt;/h2>
&lt;p>This is the first of four increasingly ridiculous premises for an isekai that I&amp;rsquo;ve watched. The
premise is in the title. It&amp;rsquo;s actually &lt;em>very&lt;/em> well done, and not what I expected based on the title.
It&amp;rsquo;s got a huge and complex world full of political intrigue, and the show goes to a lot of
unexpected places.&lt;/p>
&lt;p>I have two criticisms of this show. First, there&amp;rsquo;s just &lt;em>too much&lt;/em> stuff in it. Too many new
characters, places, relationships, etc., and it keeps on coming. A lot of stuff is introduced in a
really confusing way, with no backstory or explanation of who or what they are. Second, the pacing
isn&amp;rsquo;t great. I swear there was one episode that consisted entirely of cuts between groups of people
talking. Like no one even got up and walked around. They all just sat around and talked.&lt;/p>
&lt;p>But it&amp;rsquo;s still quite good and very creative.&lt;/p>
&lt;p>&lt;strong>Rating: 4&lt;/strong>&lt;/p>
&lt;h2 id="so-im-a-spider-so-what">So I&amp;rsquo;m a Spider, So What?&lt;/h2>
&lt;p>This time the MC is reincarnated as a spider, not a slime. If the one above is confusing and has too
much stuff, this one is &lt;em>really&lt;/em> confusing and has &lt;em>way&lt;/em> too much stuff. Again, a fun premise and a
lot of interesting ideas, but it is all over the place.&lt;/p>
&lt;p>Also, it&amp;rsquo;s one and only season ends up with absolutely nothing resolved, and I don&amp;rsquo;t think it got
renewed. So keep that in mind when deciding whether to watch it.&lt;/p>
&lt;p>&lt;strong>Rating: 3&lt;/strong>&lt;/p>
&lt;h2 id="reincarnated-as-a-sword">Reincarnated as a Sword&lt;/h2>
&lt;p>So the previous two had the MC reincarnated as monsters. In this one he&amp;rsquo;s a sword. I bet you
couldn&amp;rsquo;t figure that out from the title.&lt;/p>
&lt;p>This seems like a fairly standard progression fantasy, but it&amp;rsquo;s got a darker than average world (one
of the MCs starts out as an enslaved child). I also really like the relationship between the sword
and Fran, the 12-year old catgirl who picks him up. Instead of being gross and pervy, it&amp;rsquo;s quite
paternal, which is a nice change of pace.&lt;/p>
&lt;p>&lt;strong>Rating: 4&lt;/strong>&lt;/p>
&lt;h2 id="reborn-as-a-vending-machine-i-now-wander-the-dungeon">Reborn as a Vending Machine, I Now Wander the Dungeon&lt;/h2>
&lt;p>So the author of this one saw the previous shows, then heard someone say &amp;ldquo;it doesn&amp;rsquo;t get any sillier
than having them reincarnate as a sword&amp;rdquo;, and they were like &amp;ldquo;hold my beer &amp;hellip;&amp;rdquo;&lt;/p>
&lt;p>Yes, in this one the MC is reincarnated as a vending machine. This absolutely batshit premise leads
to something that is surprisingly creative and entertaining. The relationship between the vending
machine and the other MC, Lammis, who carries him around, is very sweet. One of the fun things about
the show is that the machine can only communicate through stock phrases like &amp;ldquo;Hello there&amp;rdquo; or &amp;ldquo;Too
bad&amp;rdquo;. But despite that he actually develops real relationships with the other characters. I look
forward to seeing what happens in future seasons.&lt;/p>
&lt;p>&lt;strong>Rating: 4&lt;/strong>&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>I&amp;rsquo;ve tried Crunchyroll in the past and it seemed like they couldn&amp;rsquo;t stream without endless
buffering even with my 1Gbps fiber to the home service. It was a huge disappointment.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Isekai is Japanese for &amp;ldquo;different world&amp;rdquo;. In these stories a person from our world is
transported to another world, usually a fantasy world. Either they&amp;rsquo;re summoned there or they die
in ours and are reborn there. A lot of them are very RPG-ish, with people talking about levels,
stats, skills, mana points, etc.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Eating Vegan in Taiwan</title><link>https://blog.urth.org/2023/12/28/eating-vegan-in-taiwan/</link><pubDate>Thu, 28 Dec 2023 11:45:00 +0800</pubDate><guid>https://blog.urth.org/2023/12/28/eating-vegan-in-taiwan/</guid><description>&lt;p>&lt;em>Last Updated: January, 2026&lt;/em>&lt;/p>
&lt;p>This is a post on finding vegan food in Taiwan, with some context on the history of vegetarian food
here. It&amp;rsquo;s &lt;em>not&lt;/em> a list of vegan restaurants. Instead, it&amp;rsquo;s a meta-post about navigating Taiwan as a
vegan.&lt;/p>
&lt;h2 id="about-me">About Me&lt;/h2>
&lt;p>I&amp;rsquo;ve been vegan since late 1996, so over 27 years at the time of this post (December, 2023). I&amp;rsquo;m
currently starting my eleventh visit to Taiwan since 2000, and I&amp;rsquo;ve spent about 9 months here total
across those visits. While I&amp;rsquo;m not fluent in Mandarin, I can speak and read a little bit. My wife is
Taiwanese, so when I need to ask more complex questions I have a translator to help me. But I also
go out to eat by myself here without my wife, so I have some sense of what it&amp;rsquo;s like to eat as a
vegan who&amp;rsquo;s not fluent in the local language.&lt;/p>
&lt;h2 id="about-you">About You&lt;/h2>
&lt;p>This post assumes you don&amp;rsquo;t speak Mandarin or read any Chinese characters. If that&amp;rsquo;s not true, then
some of this advice won&amp;rsquo;t be necessary, because you can just ask food vendors questions in Mandarin.&lt;/p>
&lt;h2 id="google-maps-lists">Google Maps Lists&lt;/h2>
&lt;p>I maintain custom Google Maps lists of vegan and vegan-friendly restaurants for various locations in
Taiwan. These are most up to date right before I visit Taiwan and while I&amp;rsquo;m here. They&amp;rsquo;re less up to
date at other times. Most of these lists were last updated between December, 2025 and
February, 2026. When I&amp;rsquo;m in Taiwan, I&amp;rsquo;m mostly in Kaohsiung, so that list is always the most up to
date.&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://maps.app.goo.gl/9kcgj6rJU6t6xuhC8">Taipei&lt;/a> - includes some places in New Taipei City.&lt;/li>
&lt;li>&lt;a href="https://maps.app.goo.gl/NwG4XxTf81G1SjUE8">TPE airport&lt;/a> - the airport itself, not the surrounding
area in Taoyuan&lt;/li>
&lt;li>&lt;a href="https://maps.app.goo.gl/T6H6KAnf1sfRz7Dd7">Taoyuan&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://maps.app.goo.gl/hWYfWXCyCe7qxj297">Taichung&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://maps.app.goo.gl/ruTwzMYQVns2V7yV8">Kaohsiung&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://maps.app.goo.gl/PoPQP85JHcyR12wE9">Hualien&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://maps.app.goo.gl/av3ZRfG71fGKULbe9">Alishan&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>These lists do not cover &lt;em>every&lt;/em> vegan-friendly place. There are literally tens of thousands of
vegetarian restaurants in Taiwan, and almost all of them have vegan food. Instead, I&amp;rsquo;ve tried to
include:&lt;/p>
&lt;ul>
&lt;li>Restaurants which are 100% vegan, particularly those with food options outside the typical
Taiwanese fare.&lt;/li>
&lt;li>Bakeries, ice cream shops, and other dessert places that I like.&lt;/li>
&lt;li>Interesting vegetarian and non-veg places that are vegan-friendly.&lt;/li>
&lt;li>Places I&amp;rsquo;ve eaten at that I really liked.&lt;/li>
&lt;/ul>
&lt;h2 id="tourists-in-taiwan">Tourists in Taiwan&lt;/h2>
&lt;p>People in Taiwan are generally quite friendly with visitors. If you learn even a little Mandarin,
people will make an effort to communicate with you. &lt;del>Sometimes they&amp;rsquo;ll even try English, but the
average worker in a restaurant or store probably speaks little to no English (or if they do they&amp;rsquo;re
not comfortable &lt;em>speaking&lt;/em> it, though they may understand some of what you say).&lt;/del>&lt;/p>
&lt;p>Edit February, 2024: I&amp;rsquo;ve realized that there&amp;rsquo;s very much a generational divide here in regards to
English. Many more younger people (&amp;lt; 40 or so) I&amp;rsquo;ve encounted in stores are willing to try speaking
English than on my first trips to Taiwan. Older people still do not speak English. That said, you&amp;rsquo;re
still going to have a better experience as a vegan if you don&amp;rsquo;t rely on English.&lt;/p>
&lt;h2 id="do-you-need-to-speak-mandarin-to-eat-vegan-in-taiwan">Do You Need to Speak Mandarin to Eat Vegan in Taiwan?&lt;/h2>
&lt;p>This is complicated, and I&amp;rsquo;ll get into this more below. The short answer is &amp;ldquo;no&amp;rdquo;, but depending on
how strict you want to be as a vegan, you may end up with a fairly limited set of food options.&lt;/p>
&lt;h2 id="learn-some-mandarin">Learn Some Mandarin&lt;/h2>
&lt;p>Learning a few phrases in Mandarin will go a long way. You can also have these written down on paper
or in an easy to access place on your phone to show people. Learn to read and pronounce
&lt;a href="https://en.wikipedia.org/wiki/Pinyin">Pinyin&lt;/a> so you can say these phrases with the proper tones.
Without the tones people may not understand you.&lt;/p>
&lt;ul>
&lt;li>&amp;ldquo;I am vegan&amp;rdquo; - 我吃全素 - Wǒ chī chúan sù (literally &amp;ldquo;I eat complete vegetarian&amp;rdquo;)
&lt;ul>
&lt;li>Note that this doesn&amp;rsquo;t &lt;em>exactly&lt;/em> mean vegan as we use the term. You should follow up with the
phrases below to tell them you don&amp;rsquo;t eat eggs or dairy just to be sure.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&amp;ldquo;I don&amp;rsquo;t eat eggs&amp;rdquo; - 我不吃蛋 - Wǒ bù chī dàn&lt;/li>
&lt;li>&amp;ldquo;I don&amp;rsquo;t eat milk, butter, cheese&amp;rdquo; - 我不吃牛奶、奶油起司 - Wǒ bù chī niúnǎi, nǎiyóu qǐ sī&lt;/li>
&lt;li>&amp;ldquo;I eat garlic and onions&amp;rdquo;&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> - 我吃大蒜和洋蔥 - Wǒ chī dàsuàn hé yángcōng&lt;/li>
&lt;li>&amp;ldquo;English menu&amp;rdquo; - 英文菜單 - Yīngwén càidān (you can just say this to ask if they have one)&lt;/li>
&lt;/ul>
&lt;p>You should also learn things like &amp;ldquo;thank you&amp;rdquo;, &amp;ldquo;excuse me&amp;rdquo;, and so on. But this isn&amp;rsquo;t a general
&amp;ldquo;Mandarin for tourists&amp;rdquo; post, which I&amp;rsquo;m sure you can find elsewhere.&lt;/p>
&lt;h2 id="learn-to-read-two-characters">Learn to Read Two Characters&lt;/h2>
&lt;p>The two most useful characters you can learn to recognize look like this:
&lt;span style="font-size: 150%">素食&lt;/span>&lt;/p>
&lt;p>This means &amp;ldquo;vegetarian food&amp;rdquo;. Many vegetarian restaurants and food carts use these characters in
their signage.&lt;/p>
&lt;h2 id="the-swastika-卍">The Swastika (卍)&lt;/h2>
&lt;p>You may also see this symbol - 卍. Note that it&amp;rsquo;s the mirror image of the version the Nazis used.
Don&amp;rsquo;t freak out! This is a Buddhist symbol, and any food vendor with this symbol will probably be
vegetarian.&lt;/p>
&lt;h2 id="tools-you-need">Tools You Need&lt;/h2>
&lt;p>Install the
&lt;a href="https://chromewebstore.google.com/detail/google-translate/aapbdbdomjkkjkaonfhkkikfgjllcleb?pli=1">Google Translate Chrome extension&lt;/a>.
If you&amp;rsquo;re using Firefox, there is
&lt;a href="https://addons.mozilla.org/en-US/firefox/addon/to-google-translate/">an unofficial extension for Google Translate&lt;/a>.
Learn how to use this on your laptop browser! It&amp;rsquo;s incredibly useful for translating websites.&lt;/p>
&lt;p>If you&amp;rsquo;re on Android, the Chrome browser already has this built in. If you&amp;rsquo;re on an iPhone,
translation is built into Safari. I&amp;rsquo;ve never owned an iPhone so I don&amp;rsquo;t know how well this works,
but I imagine it&amp;rsquo;s decent.&lt;/p>
&lt;p>Install the Google Translate
&lt;a href="https://play.google.com/store/apps/details?id=com.google.android.apps.translate">Android app&lt;/a> or
&lt;a href="https://apps.apple.com/us/app/google-translate/id414706506">iPhone app&lt;/a>. This app is fantastic! You
can point your camera at written text to get a translation, and in my recent experience this
reasonably well on menus. I&amp;rsquo;m able to &amp;ldquo;read&amp;rdquo; a Chinese menu using the app, though see below for more
detail on understanding menus.&lt;/p>
&lt;p>The Google Translate app also does real time (ish) audio translation, so you can have (slow)
conversations with people in Chinese. That said, if a restaurant is very busy the staff may not be
willing to do this.&lt;/p>
&lt;h2 id="english-menus">English Menus&lt;/h2>
&lt;p>More upscale restaurants or restaurants that are part of a chain will often have menus with both
English and Chinese. Sometimes they&amp;rsquo;ll have a separate English menu. Smaller mom and pop shops and
food carts often don&amp;rsquo;t, though in touristy areas (like the most popular night markets) they might.
But don&amp;rsquo;t &lt;em>assume&lt;/em> you&amp;rsquo;ll find one. Be prepared to use Google Translate on your phone.&lt;/p>
&lt;p>If you don&amp;rsquo;t look East Asian they may just give you the English menu or point you to where it&amp;rsquo;s
posted. If you look East Asian you&amp;rsquo;ll probably have to ask for it.&lt;/p>
&lt;h2 id="vegetarian-food-in-taiwan">Vegetarian Food in Taiwan&lt;/h2>
&lt;p>Taiwan has always had a lot of &lt;em>vegetarian&lt;/em> food, at least since I first visited in 2000. There are
many restaurants, food carts, and options in grocery stores. Historically, this is because Taiwan
has a lot of Buddhists (35% according to Wikipedia), and Taiwanese Buddhism emphasizes
vegetarianism.&lt;/p>
&lt;p>However, Taiwanese Buddhism &lt;em>does&lt;/em> allow animal products, as long as the animals aren&amp;rsquo;t directly
&lt;em>killed&lt;/em> to make the product. This means dairy has always been allowed. In the past, many Buddhist
food vendors would not use eggs, because they might have been fertilized, which means eating one
would kill the fetus. But ironically, the advent of factory farming means that eggs are &lt;em>more&lt;/em>
acceptable to Buddhists, since they cannot be fertilized when the hens live in confinement separate
from roosters&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>Buddhist practice in Taiwan also forbids garlic, onions, and anything else in
&lt;a href="https://en.wikipedia.org/wiki/Allium">the Allium genus&lt;/a>, like leeks and chives. If you go into a
non-veg restaurant and tell them you&amp;rsquo;re vegetarian in Chinese, you may end up with food that doesn&amp;rsquo;t
have any garlic or onions.&lt;/p>
&lt;h2 id="dairy-and-eggs-in-practice">Dairy and Eggs in Practice&lt;/h2>
&lt;p>Vegetarian food vendors serving Asian food mostly don&amp;rsquo;t use dairy, except in some desserts. That&amp;rsquo;s
simply because it&amp;rsquo;s not part of the cuisine. They &lt;em>do&lt;/em> often use eggs.&lt;/p>
&lt;p>Vendors selling Western food often use dairy, and it&amp;rsquo;s common to see things like pasta with cheese
in vegetarian restaurants. It&amp;rsquo;s relatively &lt;em>uncommon&lt;/em> to find a vegetarian (as opposed to vegan)
place which offers the same items with dairy alternatives. However, there are some vegan restaurants
that &lt;em>do&lt;/em> offer things like burgers or pizza with vegan cheese.&lt;/p>
&lt;h2 id="reading-menus">Reading Menus&lt;/h2>
&lt;p>At many restaurants, the menu is very minimal, containing only a title for each item without any
indication of ingredients, how it&amp;rsquo;s prepared, etc.&lt;/p>
&lt;p>To make matters even more confusing, when you use Google Translate, you sometimes get an unhelpful
translation. This is often because it&amp;rsquo;s a common dish with an idiomatic name that translates to
something like &amp;ldquo;soup, winter pink pigment&amp;rdquo;. Taiwanese people know what this is, but you won&amp;rsquo;t.&lt;/p>
&lt;p>There&amp;rsquo;s not much to be done about this. If you&amp;rsquo;re adventurous and not picky like me&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> then go
ahead and order it.&lt;/p>
&lt;p>The one thing I&amp;rsquo;d note is that if the translation includes the word &amp;ldquo;egg&amp;rdquo; or &amp;ldquo;omelet&amp;rdquo; then you
should assume that it contains eggs!&lt;/p>
&lt;h3 id="menu-markers-for-ingredients">Menu Markers for Ingredients&lt;/h3>
&lt;p>Some restaurants that use dairy and eggs mark items which contain them on the menu. Often, this is
done with graphics that look like a bottle of milk or eggs, but it&amp;rsquo;s not uncommon to see text
instead. The commonly used characters are:&lt;/p>
&lt;ul>
&lt;li>Has dairy - 奶食 (more literally translated as &amp;ldquo;milk vegetarian&amp;rdquo;)&lt;/li>
&lt;li>Has eggs - 蛋食 (more literally translated as &amp;ldquo;egg vegetarian&amp;rdquo;)&lt;/li>
&lt;li>Vegan - 全素 (more literally &amp;ldquo;complete vegetarian&amp;rdquo;) or 純素 (more literally &amp;ldquo;pure vegetarian&amp;rdquo;)&lt;/li>
&lt;li>Contains ingredients from the garlic/onion family - 植物五辛素 (more literally &amp;ldquo;five pungent herbs
vegetarian&amp;rdquo;)
&lt;ul>
&lt;li>A dish with this marker may &lt;em>also&lt;/em> contain eggs or dairy.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>You may also see the word &amp;ldquo;vegan&amp;rdquo; written in English. In my experience this is generally accurate on
menus. However, on food packaging this may not be correct. See
&lt;a href="https://vegantaiwan.blogspot.com/2009/06/new-vegetarian-labelling-coming-in.html">this blog post on Taiwanese food labeling&lt;/a>
for much more detail. I also go into this below.&lt;/p>
&lt;p>Some fancier non-veg restaurants label vegetarian items with a green leaf or some other green icon.
But most non-veg restaurants have no such labels. The only way to find out if they have vegan food
is to ask.&lt;/p>
&lt;h3 id="non-vegan-mock-meat">Non-Vegan Mock Meat&lt;/h3>
&lt;p>The mock meats used in many Taiwanese vegetarian places may not be vegan, as some mock meats are
made with eggs (or whey, I believe). However, it&amp;rsquo;s quite possible that the vendor won&amp;rsquo;t know what&amp;rsquo;s
in the them. I typically don&amp;rsquo;t worry about this too much, but if you are stricter than I am, you
should be safe by only eating tofu and mock meats made with soy or wheat gluten (aka mock duck in
the US). Avoid things like mock ham, chicken nuggets, etc.&lt;/p>
&lt;p>That said, the menu items often don&amp;rsquo;t make it clear what the mock meat is. It&amp;rsquo;s very common to see a
dish like &amp;ldquo;rice with meat sauce&amp;rdquo; at a vegetarian restaurant. Typically, this sauce will be made with
a soy product or wheat gluten and some chopped mushrooms. But it could be made with some other mock
meat.&lt;/p>
&lt;p>Also, the menu may not indicate that an item contains mock meat at all, instead the menu might just
say &amp;ldquo;fried rice&amp;rdquo; (in Chinese), but that fried rice might contain chopped mock ham.&lt;/p>
&lt;p>There&amp;rsquo;s really no good way to handle this except with a more detailed conversation in Chinese (using
Google Translate) or by only eating at 100% vegan places.&lt;/p>
&lt;h2 id="the-english-word-vegan-in-taiwan">The English Word &amp;ldquo;Vegan&amp;rdquo; in Taiwan&lt;/h2>
&lt;p>The way that Taiwanese restaurants and food vendors use the English word &amp;ldquo;vegan&amp;rdquo; can be extremely
confusing. In general, when it used as a &lt;em>label&lt;/em>, either on a menu or on product packaging, along
with the Chinese &amp;ldquo;全素&amp;rdquo;, the item in question is vegan.&lt;/p>
&lt;p>However, the word &amp;ldquo;vegan&amp;rdquo; is &lt;em>also&lt;/em> used in food &lt;em>names&lt;/em>. An example would be a product called
something like &amp;ldquo;Vegan Sweet and Sour Chicken&amp;rdquo;. While these products like this are usually
vegetarian, they are often &lt;em>not&lt;/em> vegan, and may have a label like &amp;ldquo;蛋食&amp;rdquo; (vegetarian with eggs). I&amp;rsquo;m
not sure why this happens, but my guess is that people are using Google Translate, and I don&amp;rsquo;t think
it does a great job of picking between &amp;ldquo;vegan&amp;rdquo; and &amp;ldquo;vegetarian&amp;rdquo;.&lt;/p>
&lt;p>Why are the labels correct? Because most of the people who look at them are Taiwanese people who are
eating vegetarian or vegan food for religious reasons. This is a big deal, so food vendors don&amp;rsquo;t
screw it up. But those people don&amp;rsquo;t look at &lt;em>product names in English&lt;/em> order to determine whether
they&amp;rsquo;re suitable for their religious restrictions, so the English names are confusing.&lt;/p>
&lt;p>Note that according to
&lt;a href="https://vegantaiwan.blogspot.com/2009/06/new-vegetarian-labelling-coming-in.html">this blog post on Taiwanese food labeling&lt;/a>,
7-11 has used &amp;ldquo;純素&amp;rdquo; as a &lt;em>label&lt;/em> on items that aren&amp;rsquo;t even vegetarian. Wow, so confusing. But in my
experience &amp;ldquo;全素&amp;rdquo; is still safe.&lt;/p>
&lt;h2 id="finding-places-to-eat">Finding Places to Eat&lt;/h2>
&lt;p>In my experience, the best tool for finding places to eat is Google Maps. Simply searching for
&amp;ldquo;vegan food near me&amp;rdquo; or &amp;ldquo;vegan food Taipei&amp;rdquo; finds a ton of places, including both vegan and
vegetarian options. You can also search for &amp;ldquo;素食&amp;rdquo; (vegetarian) or &amp;ldquo;全素&amp;rdquo; and &amp;ldquo;純素&amp;rdquo; (vegan).&lt;/p>
&lt;p>However, I would note that Google&amp;rsquo;s categorization of places as either &amp;ldquo;vegan restaurant&amp;rdquo; or
&amp;ldquo;vegetarian restaurant&amp;rdquo; is quite random. It&amp;rsquo;s wrong in both ways, marking vegan places as vegetarian
and vegetarian places as vegan. I&amp;rsquo;m trying to fix this a bit as I see it, but just assume it&amp;rsquo;s
wrong.&lt;/p>
&lt;p>Google Maps is used widely by Taiwanese people, so there&amp;rsquo;s a lot of reviews for most places. It&amp;rsquo;s
also the most accurate resource I&amp;rsquo;ve found for determining whether a business in still in operation.&lt;/p>
&lt;p>You may want to use &lt;a href="https://www.happycow.net/asia/taiwan/">Happy Cow&lt;/a>. From what I can tell, Happy
Cow is mostly updated by English-speaking people, most of whom are here for relatively short periods
of time, so it&amp;rsquo;s often out of date, with many listings for places that are closed, while missing
many great options. I would recommend that you always look a place up on Google Maps to confirm that
it still exists before going there. Happy Cow is also often wrong about vegetarian versus vegan.&lt;/p>
&lt;p>If you Google for &amp;ldquo;vegan food in Taiwan&amp;rdquo; you&amp;rsquo;ll find a lot of blog posts. Again, these are often
quite out of date. Confirm that the places mentioned still exist before going there!&lt;/p>
&lt;p>There are also some directory/map sites in Chinese:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://food.suiis.com/">https://food.suiis.com/&lt;/a> - In my experience this one is sometimes out of date and it will show you
places that are closed, but it can help you find places that don&amp;rsquo;t show up with an English
language search.&lt;/li>
&lt;li>&lt;a href="https://vegemap.merit-times.com/restaurant_list">https://vegemap.merit-times.com/restaurant_list&lt;/a> - I have no idea how up to date this is. I haven&amp;rsquo;t
used it much.&lt;/li>
&lt;li>&lt;a href="https://ifoodie.tw/">https://ifoodie.tw/&lt;/a> - You can search for &amp;ldquo;素食&amp;rdquo; in various cities. I have no idea how up to date
or complete this site is. I haven&amp;rsquo;t used it much.&lt;/li>
&lt;/ul>
&lt;p>Again, I recommend you double check any listing you find against Google Maps, which is the most up
to date resource I&amp;rsquo;ve found for whether a business is closed or not.&lt;/p>
&lt;p>&lt;strong>Note that Google Maps is often wrong about the hours of operation.&lt;/strong> Many small restaurants have
random days off during the month. A lot of them will post a calendar image to Facebook or Instagram
each month with the days they&amp;rsquo;re closed marked on it. I highly recommend checking out these posts
for both the days they&amp;rsquo;re closed and their actual hours of operation.&lt;/p>
&lt;h3 id="convenience-stores">Convenience Stores&lt;/h3>
&lt;p>Unlike in the US, convenience stores here in Taiwan actually have good food! A lot of them have
vegan items. But see my section above about the use of the word &amp;ldquo;vegan&amp;rdquo;, and
&lt;a href="https://vegantaiwan.blogspot.com/2009/06/new-vegetarian-labelling-coming-in.html">this blog post on Taiwanese food labeling&lt;/a>
for more details.&lt;/p>
&lt;p>FamilyMart has multiple flavors of delicious vegan rice balls. They also offer frozen food products
and instant noodles. I think they will prepare these for you in the store and you can eat there.
7-11 also has some similar items, but I haven&amp;rsquo;t seen vegan rice balls there.&lt;/p>
&lt;h2 id="advice-about-specific-food-items">Advice About Specific Food Items&lt;/h2>
&lt;ul>
&lt;li>Stinky tofu - This may not be vegan. Animal products are often used to start the fermentation
process, including animal blood or dairy, so even a &amp;ldquo;vegetarian stinky tofu&amp;rdquo; vendor may not be
vegan. You&amp;rsquo;ll have to ask.&lt;/li>
&lt;li>Scallion pancakes - These are often made with lard, but some are made with vegetable shortening.&lt;/li>
&lt;li>Sticky rice - Sometimes made with lard.&lt;/li>
&lt;li>Anything deep fried or boiled. This may be cooked in the same oil or water as non-vegan food if
the vendor serves non-vegan items.&lt;/li>
&lt;li>Bubble tea - Even though oat milk is common at coffee shops it&amp;rsquo;s rarely found at bubble tea shops,
sadly. But bubble tea shops in Taiwan have many milk-free options.&lt;/li>
&lt;li>Coffee and tea - If you go to a coffee shop as opposed to a bubble tea shop, many of them have oat
milk for lattes and tea drinks. Starbucks has multiple plant milk options. Louisa has oat milk.&lt;/li>
&lt;/ul>
&lt;h2 id="other-resources">Other Resources&lt;/h2>
&lt;p>Check out
&lt;a href="https://www.taiwanobsessed.com/vegan-vegetarian-food-in-taiwan/">Nick Kembel&amp;rsquo;s post on the same topic as this one&lt;/a>.
It covers some things I don&amp;rsquo;t, like how to order using paper menus as well as more information on
vegetarian (but not vegan) options.&lt;/p>
&lt;h2 id="the-absolute-best-way-to-find-vegan-food-in-taiwan">The Absolute Best Way to Find Vegan Food in Taiwan&lt;/h2>
&lt;p>The absolute best way to find vegan food in Taiwan is with a Taiwanese person! I went so far as to
marry one. That&amp;rsquo;s pretty extreme but you may want to consider this option if you struggle with
finding vegan food in Taiwan.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>I will explain why this phrase is useful below.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>A great demonstration of how deontological ethics fails without some consequentialism to go with
it.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>I hate mushrooms!&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Naming Your Binary Executable Releases</title><link>https://blog.urth.org/2023/04/16/naming-your-binary-executable-releases/</link><pubDate>Sun, 16 Apr 2023 17:50:12 -0500</pubDate><guid>https://blog.urth.org/2023/04/16/naming-your-binary-executable-releases/</guid><description>&lt;p>&lt;a href="https://github.com/houseabsolute/ubi">My universal binary installer tool, &lt;code>ubi&lt;/code>&lt;/a>, has to deal with
a lot of &amp;ldquo;interesting&amp;rdquo; decisions when it comes to how people name their releases on GitHub.&lt;/p>
&lt;p>So in the interests of making the world of binary executable releases more machine-readable and a
little less weird&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>, here are my recommendations on naming your release files. The TLDR is:&lt;/p>
&lt;ul>
&lt;li>Either use an extension or don&amp;rsquo;t include periods in the filename.&lt;/li>
&lt;li>Use well-known operating system and CPU architecture names as part of the filename.&lt;/li>
&lt;/ul>
&lt;p>This applies to releases which just contain a single &lt;em>compiled&lt;/em> executable (and maybe a readme or
license file, but the binary is the important bit). This doesn&amp;rsquo;t apply to anything which ships its
own installer, nor does it apply to files that contain packages, like debs or RPMs. It also doesn&amp;rsquo;t
apply to programs in interpreted languages like Python or Perl, unless you are actually shipping a
single-file binary using something like &lt;code>py2exe&lt;/code>&lt;/p>
&lt;h2 id="either-include-an-extension-or-dont-include-any-periods">Either Include an Extension or Don&amp;rsquo;t Include Any Periods&lt;/h2>
&lt;p>Some folks want to release the bare binary without any compression. That&amp;rsquo;s fine. But if you do this,
please &lt;strong>don&amp;rsquo;t put any periods in the filename unless it also has an actual file extension.&lt;/strong>&lt;/p>
&lt;p>The reason for this is that it&amp;rsquo;s very hard to determine whether &amp;ldquo;the text after the period&amp;rdquo; is a
file extension or just part of the main filename. For example, given the filename
&lt;code>my-cool-program-v1.2.3-linux-x86-64&lt;/code>, what&amp;rsquo;s the file extension? Well, you and I, as humans&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>,
can tell that there is no extension. But for a computer, it sure looks like this file has an
extension of &lt;code>.3-linux-x86-64&lt;/code>!&lt;/p>
&lt;p>So either give the file a proper extension, like &lt;code>.exe&lt;/code> on Windows, or avoid periods in the name
entirely.&lt;/p>
&lt;p>A simple way to make sure all your files have an extension is to just compress all of them. What I
typically see is &lt;code>.gz&lt;/code> for Unix-y systems and &lt;code>.zip&lt;/code> for Windows, but any sort of relatively common
compression scheme is fine.&lt;/p>
&lt;h2 id="include-the-operating-system-and-cpu-architecture-in-the-filename">Include the Operating System and CPU Architecture in the Filename&lt;/h2>
&lt;p>&lt;strong>And don&amp;rsquo;t make up your own operating system and architecture naming scheme!&lt;/strong>&lt;/p>
&lt;p>Don&amp;rsquo;t use names like &amp;ldquo;linux64&amp;rdquo; or &amp;ldquo;win32&amp;rdquo;. Please include both the OS name and the CPU architecture
name as separate components instead of some shorthand for both.&lt;/p>
&lt;p>There&amp;rsquo;s no standard for this but there are many reasonable sources for this information.&lt;/p>
&lt;ul>
&lt;li>Running &lt;code>uname -p&lt;/code> on systems that support this will give you a reasonable architecture name.&lt;/li>
&lt;li>Running &lt;code>uname -s&lt;/code> gives you a reasonable OS name.&lt;/li>
&lt;li>With Rust, the &amp;ldquo;target triple&amp;rdquo; for the &lt;code>rustc&lt;/code> command can be taken from &lt;code>rustc -vV&lt;/code>. You can use
this target triple directly as part of the filename.&lt;/li>
&lt;li>With Go, you can run &lt;code>go version&lt;/code>, which will print something like
&lt;code>go version go1.18.5 linux/amd64&lt;/code>. Split that last bit on the &lt;code>/&lt;/code> to get the OS name and CPU
architecture.
&lt;ul>
&lt;li>If you&amp;rsquo;re cross-compiling Go then you have to set the &lt;code>GOOS&lt;/code> and &lt;code>GOARCH&lt;/code> environment variables.
You can use the contents of those variables in the filename too!&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>When you run &lt;code>gcc -v&lt;/code> it prints a line like &lt;code>--target=x86_64-linux-gnu&lt;/code>, where the target includes
both the CPU architecture and OS name (at least on my system with GCC 9.4.0).&lt;/li>
&lt;li>Running &lt;code>clang --version&lt;/code> prints a line like &lt;code>Target: x86_64-pc-linux-gnu&lt;/code> with similar info.&lt;/li>
&lt;/ul>
&lt;p>Other languages often include this in their version output, like in &lt;code>perl -V&lt;/code>.&lt;/p>
&lt;p>There are probably lots of other ways to get this information. The point is that you don&amp;rsquo;t need to
make this up and hard-code an arbitrary string for each platform you build on. You can get a name
that&amp;rsquo;s useful one way or another.&lt;/p>
&lt;p>Different languages and tools don&amp;rsquo;t agree on &lt;em>exactly&lt;/em> what to call various things, so we end up
with &amp;ldquo;Darwin&amp;rdquo; and &amp;ldquo;macOS&amp;rdquo;, &amp;ldquo;x86-64&amp;rdquo; and &amp;ldquo;amd64&amp;rdquo;, etc. But there&amp;rsquo;s a fairly limited set of variations
to account for when using the output from other programs. But if you just make stuff up on your own
the variations are limitless, and that&amp;rsquo;s not a good thing!&lt;/p>
&lt;p>You might be tempted not to include this because your program only works on one OS/CPU target. But
please don&amp;rsquo;t skip this. It will &lt;em>still&lt;/em> be useful for tools like &lt;code>ubi&lt;/code> to have this info in the
filename. And who knows, you might end up expanding the set of covered platforms in the future.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>I&amp;rsquo;m mostly &lt;em>against&lt;/em> making the world less weird, but I&amp;rsquo;ll compromise for machine readability.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>I am definitely &lt;em>not&lt;/em> an alien or a dog in a human suit and I can prove it. Woof.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Come (Maybe) Be the Boss of Me</title><link>https://blog.urth.org/2023/03/21/come-maybe-be-the-boss-of-me/</link><pubDate>Tue, 21 Mar 2023 09:31:24 -0500</pubDate><guid>https://blog.urth.org/2023/03/21/come-maybe-be-the-boss-of-me/</guid><description>&lt;p>When I started at MongoDB in May of 2022 I was the fourth person on my team. Since then, we&amp;rsquo;ve hired
&lt;em>six&lt;/em> more engineers, bringing us to a total of 10 people. That&amp;rsquo;s a big team!&lt;/p>
&lt;p>That&amp;rsquo;s why &lt;a href="https://grnh.se/1778bd4c1us">we are hiring for a new Team Lead&lt;/a>, so that we can split
into two teams. I&amp;rsquo;m not sure which team I&amp;rsquo;ll end up on, but this is your chance to maybe be my new
boss! If telling me what to do appeals to you then you should
&lt;a href="https://grnh.se/1778bd4c1us">apply right away&lt;/a>.&lt;/p>
&lt;p>Note that &lt;strong>this job is available remotely in North America&lt;/strong>.&lt;/p>
&lt;p>Also, &lt;strong>Engineering Team Leads at MongoDB still write code&lt;/strong> in addition to their management duties.
Teams are generally smaller (you&amp;rsquo;d have 5 direct reports) for that reason. So if you&amp;rsquo;re interested
in management but don&amp;rsquo;t want to give up coding, this is a great opportunity.&lt;/p>
&lt;p>I&amp;rsquo;ve been really happy working at MongoDB for the last ten months. If you have any questions about
the company, team, or product, please &lt;a href="mailto:autarch@urth.org">reach out to me&lt;/a>. I&amp;rsquo;m happy to chat
via email, phone, or video about my experience at MongoDB and any other topic.&lt;/p></description></item><item><title>Sleep No More Is My New Favorite Videogame</title><link>https://blog.urth.org/2023/03/17/sleep-no-more-is-my-new-favorite-videogame/</link><pubDate>Fri, 17 Mar 2023 13:58:59 -0400</pubDate><guid>https://blog.urth.org/2023/03/17/sleep-no-more-is-my-new-favorite-videogame/</guid><description>&lt;p>Last Saturday my wife and I saw &lt;a href="https://mckittrickhotel.com/sleep-no-more/">Sleep No More&lt;/a> in New
York City. It&amp;rsquo;s a mostly silent film noir style adaptation of Macbeth as a play/dance piece. There
are no seats. Instead you follow the performers around the space, which is four floors of a
converted hotel. You can walk through nearly all of the sets in full, and you can more or less go
where you want. You don&amp;rsquo;t even have to follow any performers at all if you don&amp;rsquo;t want to.&lt;/p>
&lt;p>I really loved it and I&amp;rsquo;m already planning to go back in the future. As I was thinking about &lt;em>why&lt;/em> I
loved it, I realized that it tickles the same part of my brain as some videogames. In particular, I
think it&amp;rsquo;s a lot like the experience of playing &lt;a href="https://bethesda.net/game/dishonored">Dishonored&lt;/a>
and similar stealth games.&lt;/p>
&lt;p>When I play a game like Dishonored, there are two aspects of it I enjoy the most. The first is
simply exploring the spaces that the game gives you. I find it incredibly rewarding to form a map of
a new space in my head, and to look into all the nooks and crannies for fun visual details. Sleep No
More lets you do &lt;em>exactly&lt;/em> the same thing. You can wander around the whole space, which is fairly
large and filled with small visual details. I honestly think I could spend many hours just looking
through the rooms without the performers there.&lt;/p>
&lt;p>The other fun part of Dishonored is observing the stories of the characters around you. If you&amp;rsquo;re
playing the game in stealth mode&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> you will spend a fair bit of time following the NPCs around,
listening to their conversations and learning their routines. Simply doing this and staying out of
sight is really fun. Sleep No More gives you a similar experience, though you don&amp;rsquo;t need to stay out
of sight.&lt;/p>
&lt;p>So I&amp;rsquo;m definitely planning to attend Sleep No More again, and this time I&amp;rsquo;m aiming for 100%
completion with all achievements!&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>This is the only &lt;em>proper&lt;/em> way to play it in my opinion.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Cross Compiling Rust Projects in GitHub Actions</title><link>https://blog.urth.org/2023/03/05/cross-compiling-rust-projects-in-github-actions/</link><pubDate>Sun, 05 Mar 2023 15:28:32 -0600</pubDate><guid>https://blog.urth.org/2023/03/05/cross-compiling-rust-projects-in-github-actions/</guid><description>&lt;aside>
&lt;small>
The highlight of all this is
&lt;a href="https://github.com/marketplace/actions/build-rust-projects-with-cross">my brand new GitHub Action for cross-compiling Rust projects&lt;/a>.
The rest of the post is about why and how I wrote it.
&lt;/small>
&lt;/aside>
&lt;p>I was recently working on the CI setup for &lt;a href="https://github.com/houseabsolute/ubi">my &lt;code>ubi&lt;/code> project&lt;/a>
with a couple goals. First, I wanted to stop using
&lt;a href="https://github.com/actions-rs/toolchain/issues/216">unmaintained&lt;/a> actions from
&lt;a href="https://github.com/actions-rs">the &lt;code>actions-rs&lt;/code> organization&lt;/a>. Second, I wanted to add many more
release targets for different platforms and architectures&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>Replacing some of what I used from &lt;code>actions-rs&lt;/code> was pretty easy:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/dtolnay/rust-toolchain">&lt;code>dtolnay/rust-toolchain&lt;/code>&lt;/a> replaces
&lt;code>actions-rs/toolchain&lt;/code>.&lt;/li>
&lt;li>&lt;a href="https://github.com/actions-rust-lang/audit">&lt;code>actions-rust-lang/audit&lt;/code>&lt;/a> replaces
&lt;code>actions-rs/audit&lt;/code>.&lt;/li>
&lt;/ul>
&lt;p>But what about &lt;a href="https://github.com/actions-rs/cargo">&lt;code>actions-rs/cargo&lt;/code>&lt;/a>? You&amp;rsquo;d think that running
&lt;code>cargo&lt;/code> wouldn&amp;rsquo;t even &lt;em>need&lt;/em> an action, and you&amp;rsquo;d be right. Except that this action doesn&amp;rsquo;t just run
&lt;code>cargo&lt;/code>. If you set its &lt;code>use-cross&lt;/code> parameter to true it uses
&lt;a href="https://github.com/rust-embedded/cross">&lt;code>cross&lt;/code>&lt;/a> to do the build instead of &lt;code>cargo&lt;/code>, making it
trivial to cross-compile a Rust project.&lt;/p>
&lt;p>I was already doing some cross-compilation for all my Rust projects, and I wanted to add more. So I
needed to replace this action with something of my own. I couldn&amp;rsquo;t find any already written,
probably because everyone who moved away from &lt;code>actions-rs&lt;/code> kept saying things like &amp;ldquo;this is too
trivial to need an action, it&amp;rsquo;s just running &lt;code>cargo build&lt;/code>.&amp;rdquo;&lt;/p>
&lt;p>So for my first pass, I simply embedded the build pieces directly in the GitHub workflow for UBI,
like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt"> 10
&lt;/span>&lt;span class="lnt"> 11
&lt;/span>&lt;span class="lnt"> 12
&lt;/span>&lt;span class="lnt"> 13
&lt;/span>&lt;span class="lnt"> 14
&lt;/span>&lt;span class="lnt"> 15
&lt;/span>&lt;span class="lnt"> 16
&lt;/span>&lt;span class="lnt"> 17
&lt;/span>&lt;span class="lnt"> 18
&lt;/span>&lt;span class="lnt"> 19
&lt;/span>&lt;span class="lnt"> 20
&lt;/span>&lt;span class="lnt"> 21
&lt;/span>&lt;span class="lnt"> 22
&lt;/span>&lt;span class="lnt"> 23
&lt;/span>&lt;span class="lnt"> 24
&lt;/span>&lt;span class="lnt"> 25
&lt;/span>&lt;span class="lnt"> 26
&lt;/span>&lt;span class="lnt"> 27
&lt;/span>&lt;span class="lnt"> 28
&lt;/span>&lt;span class="lnt"> 29
&lt;/span>&lt;span class="lnt"> 30
&lt;/span>&lt;span class="lnt"> 31
&lt;/span>&lt;span class="lnt"> 32
&lt;/span>&lt;span class="lnt"> 33
&lt;/span>&lt;span class="lnt"> 34
&lt;/span>&lt;span class="lnt"> 35
&lt;/span>&lt;span class="lnt"> 36
&lt;/span>&lt;span class="lnt"> 37
&lt;/span>&lt;span class="lnt"> 38
&lt;/span>&lt;span class="lnt"> 39
&lt;/span>&lt;span class="lnt"> 40
&lt;/span>&lt;span class="lnt"> 41
&lt;/span>&lt;span class="lnt"> 42
&lt;/span>&lt;span class="lnt"> 43
&lt;/span>&lt;span class="lnt"> 44
&lt;/span>&lt;span class="lnt"> 45
&lt;/span>&lt;span class="lnt"> 46
&lt;/span>&lt;span class="lnt"> 47
&lt;/span>&lt;span class="lnt"> 48
&lt;/span>&lt;span class="lnt"> 49
&lt;/span>&lt;span class="lnt"> 50
&lt;/span>&lt;span class="lnt"> 51
&lt;/span>&lt;span class="lnt"> 52
&lt;/span>&lt;span class="lnt"> 53
&lt;/span>&lt;span class="lnt"> 54
&lt;/span>&lt;span class="lnt"> 55
&lt;/span>&lt;span class="lnt"> 56
&lt;/span>&lt;span class="lnt"> 57
&lt;/span>&lt;span class="lnt"> 58
&lt;/span>&lt;span class="lnt"> 59
&lt;/span>&lt;span class="lnt"> 60
&lt;/span>&lt;span class="lnt"> 61
&lt;/span>&lt;span class="lnt"> 62
&lt;/span>&lt;span class="lnt"> 63
&lt;/span>&lt;span class="lnt"> 64
&lt;/span>&lt;span class="lnt"> 65
&lt;/span>&lt;span class="lnt"> 66
&lt;/span>&lt;span class="lnt"> 67
&lt;/span>&lt;span class="lnt"> 68
&lt;/span>&lt;span class="lnt"> 69
&lt;/span>&lt;span class="lnt"> 70
&lt;/span>&lt;span class="lnt"> 71
&lt;/span>&lt;span class="lnt"> 72
&lt;/span>&lt;span class="lnt"> 73
&lt;/span>&lt;span class="lnt"> 74
&lt;/span>&lt;span class="lnt"> 75
&lt;/span>&lt;span class="lnt"> 76
&lt;/span>&lt;span class="lnt"> 77
&lt;/span>&lt;span class="lnt"> 78
&lt;/span>&lt;span class="lnt"> 79
&lt;/span>&lt;span class="lnt"> 80
&lt;/span>&lt;span class="lnt"> 81
&lt;/span>&lt;span class="lnt"> 82
&lt;/span>&lt;span class="lnt"> 83
&lt;/span>&lt;span class="lnt"> 84
&lt;/span>&lt;span class="lnt"> 85
&lt;/span>&lt;span class="lnt"> 86
&lt;/span>&lt;span class="lnt"> 87
&lt;/span>&lt;span class="lnt"> 88
&lt;/span>&lt;span class="lnt"> 89
&lt;/span>&lt;span class="lnt"> 90
&lt;/span>&lt;span class="lnt"> 91
&lt;/span>&lt;span class="lnt"> 92
&lt;/span>&lt;span class="lnt"> 93
&lt;/span>&lt;span class="lnt"> 94
&lt;/span>&lt;span class="lnt"> 95
&lt;/span>&lt;span class="lnt"> 96
&lt;/span>&lt;span class="lnt"> 97
&lt;/span>&lt;span class="lnt"> 98
&lt;/span>&lt;span class="lnt"> 99
&lt;/span>&lt;span class="lnt">100
&lt;/span>&lt;span class="lnt">101
&lt;/span>&lt;span class="lnt">102
&lt;/span>&lt;span class="lnt">103
&lt;/span>&lt;span class="lnt">104
&lt;/span>&lt;span class="lnt">105
&lt;/span>&lt;span class="lnt">106
&lt;/span>&lt;span class="lnt">107
&lt;/span>&lt;span class="lnt">108
&lt;/span>&lt;span class="lnt">109
&lt;/span>&lt;span class="lnt">110
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">release&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Release - ${{ matrix.platform.os_name }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">startsWith( github.ref, &amp;#39;refs/tags/v&amp;#39; ) || github.ref == &amp;#39;refs/tags/test-release&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">strategy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matrix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">os_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">FreeBSD-x86_64&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">os&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-20.04&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x86_64-unknown-freebsd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bin&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubi-FreeBSD-x86_64.tar.gz&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cross&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cargo_command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">./cross&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">os_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Linux-x86_64&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">os&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-20.04&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x86_64-unknown-linux-musl&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bin&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubi-Linux-x86_64-musl.tar.gz&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cross&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cargo_command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cargo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">os_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Windows-aarch64&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">os&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">windows-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">aarch64-pc-windows-msvc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bin&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubi.exe&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubi-Windows-aarch64.zip&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cross&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cargo_command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cargo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">os_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">macOS-x86_64&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">os&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">macOS-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x86_64-apple-darwin&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bin&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubi-Darwin-x86_64.tar.gz&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cross&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cargo_command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cargo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ matrix.platform.os }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Checkout&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/checkout@v3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Install toolchain if not cross-compiling&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dtolnay/rust-toolchain@stable&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ matrix.platform.target }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ !matrix.platform.cross }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Install musl-tools on Linux&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sudo apt-get update --yes &amp;amp;&amp;amp; sudo apt-get install --yes musl-tools&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">contains(matrix.platform.os, &amp;#39;ubuntu&amp;#39;) &amp;amp;&amp;amp; !matrix.platform.cross&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Install cross if cross-compiling (*nix)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cross-nix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">shell&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bash&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> set -e
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> export TARGET=&amp;#34;$HOME/bin&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> mkdir -p &amp;#34;$TARGET&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ./bootstrap/bootstrap-ubi.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;$HOME/bin/ubi&amp;#34; --project cross-rs/cross --matching musl --in .&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">matrix.platform.cross &amp;amp;&amp;amp; !contains(matrix.platform.os, &amp;#39;windows&amp;#39;)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Install cross if cross-compiling (Windows)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cross-windows&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">shell&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">powershell&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> .\bootstrap\bootstrap-ubi.ps1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> .\ubi --project cross-rs/cross --in .&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">matrix.platform.cross &amp;amp;&amp;amp; contains(matrix.platform.os, &amp;#39;windows&amp;#39;)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Build binary (*nix)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">shell&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bash&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ${{ matrix.platform.cargo_command }} build --locked --release --target ${{ matrix.platform.target }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ !contains(matrix.platform.os, &amp;#39;windows&amp;#39;) }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Build binary (Windows)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># We have to use the platform&amp;#39;s native shell. If we use bash on&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Windows then OpenSSL complains that the Perl it finds doesn&amp;#39;t use&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># the platform&amp;#39;s native paths and refuses to build.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">shell&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">powershell&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;amp; ${{ matrix.platform.cargo_command }} build --locked --release --target ${{ matrix.platform.target }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">contains(matrix.platform.os, &amp;#39;windows&amp;#39;)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Strip binary&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">shell&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bash&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> strip target/${{ matrix.platform.target }}/release/${{ matrix.platform.bin }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># strip doesn&amp;#39;t work with cross-arch binaries on Linux or Windows.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ !(matrix.platform.cross || matrix.platform.target == &amp;#39;aarch64-pc-windows-msvc&amp;#39;) }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Package as archive&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">shell&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bash&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> cd target/${{ matrix.platform.target }}/release
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> if [[ &amp;#34;${{ matrix.platform.os }}&amp;#34; == &amp;#34;windows-latest&amp;#34; ]]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 7z a ../../../${{ matrix.platform.name }} ${{ matrix.platform.bin }}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> else
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> tar czvf ../../../${{ matrix.platform.name }} ${{ matrix.platform.bin }}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> cd -&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Publish release artifacts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/upload-artifact@v3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubi-${{ matrix.platform.os_name }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;ubi*&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github.ref == &amp;#39;refs/tags/test-release&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Publish GitHub release&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">softprops/action-gh-release@v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">draft&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">files&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;ubi*&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">body_path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Changes.md&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">startsWith( github.ref, &amp;#39;refs/tags/v&amp;#39; )&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I&amp;rsquo;ve actually cut quite a bit of this out, notably the other 15 or so platforms in the matrix.&lt;/p>
&lt;p>Here are the highlights:&lt;/p>
&lt;ol>
&lt;li>If it needs &lt;code>cross&lt;/code> it will install that from its latest GitHub release using &lt;code>ubi&lt;/code> itself. This
is much faster than compiling &lt;code>cross&lt;/code> by running &lt;code>cargo install&lt;/code>. Otherwise it uses
&lt;code>dtolnay/rust-toolchain&lt;/code> to install the Rust toolchain.&lt;/li>
&lt;li>It will run the build command (&lt;code>cross&lt;/code> or &lt;code>cargo&lt;/code>) in the appropriate shell for the platform.
Using the right shell matters for some corner cases. Notably, &lt;code>ubi&lt;/code> now depends on
&lt;a href="https://lib.rs/crates/openssl">the &lt;code>openssl&lt;/code> crate&lt;/a> with the &lt;code>vendored&lt;/code> feature enabled. With
that feature, the crate will actually compile OpenSSL and statically link it into your binary.
But OpenSSL fails to compile in an msys shell on Windows!&lt;/li>
&lt;/ol>
&lt;p>And that&amp;rsquo;s really it. I&amp;rsquo;ve extracted the generic bits and turned it into a reusable action called
&lt;a href="https://github.com/marketplace/actions/build-rust-projects-with-cross">Build Rust Projects with Cross&lt;/a>.&lt;/p>
&lt;p>You can see it in use in
&lt;a href="https://github.com/houseabsolute/ubi/blob/master/.github/workflows/ci.yml#L50-L220">the release job for &lt;code>ubi&lt;/code>&lt;/a>.
The YAML for &lt;code>precious&lt;/code> is nearly identical (which suggests that maybe I need to write &lt;em>another&lt;/em>
action).&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>For &lt;a href="https://github.com/houseabsolute/ubi/releases/tag/v0.0.20">the 0.0.20 release&lt;/a>, there are
published binaries for 20 different OS/CPU targets, including many for Linux, some for Windows
and macOS, and one each for FreeBSD and NetBSD.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>All My Perl Modules Are in Maintenance Mode</title><link>https://blog.urth.org/2023/02/11/all-my-perl-modules-are-in-maintenance-mode/</link><pubDate>Sat, 11 Feb 2023 12:06:14 -0600</pubDate><guid>https://blog.urth.org/2023/02/11/all-my-perl-modules-are-in-maintenance-mode/</guid><description>&lt;p>This is probably obvious to anyone paying attention to my CPAN releases over the past few years, but
in case it&amp;rsquo;s not, I wanted to state this clearly. &lt;strong>All of
&lt;a href="https://metacpan.org/author/DROLSKY">my Perl modules&lt;/a> are in maintenance mode.&lt;/strong>&lt;/p>
&lt;p>Why? I no longer do any professional work with Perl, and I haven&amp;rsquo;t done any since 2017 or so. All of
my most enjoyable personal projects are in &lt;a href="https://www.rust-lang.org/">Rust&lt;/a> these days. Also, I&amp;rsquo;m
a bit burned out on the Perl community. I think as it&amp;rsquo;s shrunk some of the most unpleasant aspects
of it have been amplified for me, and my time on the Perl and Raku Foundation board exposed me to
even more of this, further burning me out. Please note that &lt;strong>I&amp;rsquo;m not talking about TPRF&amp;rsquo;s board or
volunteers here!&lt;/strong>&lt;/p>
&lt;p>So here&amp;rsquo;s what &amp;ldquo;maintenance mode&amp;rdquo; means to me &amp;hellip;&lt;/p>
&lt;p>&lt;strong>I will not &lt;em>personally&lt;/em> work on significant new features.&lt;/strong> The definition of &amp;ldquo;significant&amp;rdquo; here
is nebulous, but basically, if it takes more then a few hours of effort, I&amp;rsquo;m probably not going to
work on it.&lt;/p>
&lt;p>I &lt;em>may&lt;/em> review PRs for significant features, but honestly, I probably won&amp;rsquo;t have the motivation for
this. If you want to propose a significant new feature that requires careful review, then the best
way to get this to be merged would be to &lt;em>also&lt;/em> find some reviewers to look at it. If someone
submits a bigger PR and a few other people review it that I trust&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>, I&amp;rsquo;m a lot more likely to
merge it.&lt;/p>
&lt;p>&lt;strong>I will not &lt;em>personally&lt;/em> make releases that break backward compatibility for most of my modules.&lt;/strong>
I qualify this with &amp;ldquo;most&amp;rdquo; because there are some things that aren&amp;rsquo;t widely used that I&amp;rsquo;m okay with
breaking, for example, my
&lt;a href="https://metacpan.org/dist/Dist-Zilla-PluginBundle-DROLSKY">personal &lt;code>Dist::Zilla&lt;/code> bundle&lt;/a> and other
things where I&amp;rsquo;m the main (or only) user.&lt;/p>
&lt;p>&lt;strong>I will &lt;em>probably&lt;/em> not hand off ownership of widely used modules.&lt;/strong> Some of my code is &lt;em>very&lt;/em>
widely used, including things like &lt;a href="https://metacpan.org/dist/DateTime">&lt;code>DateTime&lt;/code>&lt;/a> and
&lt;a href="https://metacpan.org/dist/Log-Dispatch">&lt;code>Log-Dispatch&lt;/code>&lt;/a>. I&amp;rsquo;m very wary of giving others control of
these since breakage in one of them can break a lot of CPAN or a lot of existing applications. &lt;strong>I
&lt;em>do&lt;/em> welcome help with maintenance in the form of PRs and PR reviews!&lt;/strong>&lt;/p>
&lt;p>This includes modules like &lt;a href="https://metacpan.org/dist/Specio">&lt;code>Specio&lt;/code>&lt;/a>, where it&amp;rsquo;s in wide use
entirely because it&amp;rsquo;s used in &lt;code>DateTime&lt;/code>.&lt;/p>
&lt;p>Given all this, if for example, you have a suggestion for a &lt;code>DateTime&lt;/code> 2.0 feature, I think the best
option is to fork &lt;code>DateTime&lt;/code> (into a namespace not starting with &lt;code>DateTime&lt;/code>) and go ham on it.&lt;/p>
&lt;p>&lt;strong>I will &lt;em>try&lt;/em> to keep my Perl modules working with newer Perl versions.&lt;/strong> So far this has generally
not been too difficult. If that changes, I will reconsider my stance on handing over ownership.&lt;/p>
&lt;p>&lt;strong>I will &lt;em>try&lt;/em> to fix bugs in code and land mines in APIs. I will do this as long as they can be
fixed in a backward-compatible way, without major surgery on the code, in a reasonable amount of my
time.&lt;/strong> Backwards compatibility trumps correctness here, especially if the buggy behavior is
documented. This goes double for &amp;ldquo;bugs&amp;rdquo; of the form &amp;ldquo;I don&amp;rsquo;t like this documented&amp;rdquo; behavior. That&amp;rsquo;s
not a bug, that&amp;rsquo;s a design decision you disagree with. See above about forking.&lt;/p>
&lt;p>One safe way to fix land mines is to add new methods/functions/parameters, like I&amp;rsquo;ve done with
&lt;a href="https://metacpan.org/dist/Time-Local">&lt;code>Time::Local&lt;/code>&lt;/a>. &lt;strong>PRs to do this sort of thing are welcome
and encouraged&lt;/strong>, with the caveat about significant features from above.&lt;/p>
&lt;p>How long will I continue to do this? I don&amp;rsquo;t know. I&amp;rsquo;m amazed that people are still using code I
wrote over 20 years ago! Will they still be using it 20 years from &lt;em>now&lt;/em>? Maybe. Will there be new
versions of Perl to deal with then? Will I still want to touch any of this code? I have no clue.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>I can&amp;rsquo;t list all the people I trust. But I&amp;rsquo;d say that a good reviewer is someone with a long
history of using Perl who understands
&lt;a href="http://neilb.org/2015/04/20/river-of-cpan.html">The River of CPAN&lt;/a> metaphor. If you want to
know if someone would be a good reviewer, you can &lt;a href="mailto:autarch@urth.org">email me to ask&lt;/a> or
find me on the TPRF Slack.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Big Changes in Precious v0.4.0</title><link>https://blog.urth.org/2022/11/19/big-changes-in-precious-v0-4-0/</link><pubDate>Sat, 19 Nov 2022 16:24:35 -0600</pubDate><guid>https://blog.urth.org/2022/11/19/big-changes-in-precious-v0-4-0/</guid><description>&lt;p>I just
&lt;a href="https://github.com/houseabsolute/precious/releases/tag/v0.4.0">released version 0.4.0 of &lt;code>precious&lt;/code>&lt;/a>,
my code quality meta-tool for configuring a collection of linters and tidiers for a project.&lt;/p>
&lt;p>The headline change for this release is that command invocation configuration has changed, with the
old &lt;code>run_mode&lt;/code> and &lt;code>chdir&lt;/code> keys being deprecated. Don&amp;rsquo;t worry, you can safely upgrade to this
release, as the old config keys still work and do not cause &lt;code>precious&lt;/code> to emit any warning yet&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>Also, &lt;code>precious&lt;/code> still works the same way by default as it always did. It runs the command once per
file from the project root, passing the command the file&amp;rsquo;s path relative to that root.&lt;/p>
&lt;p>The problem with the old configuration is that it didn&amp;rsquo;t really capture the full scope of possible
ways to invoke commands. Specifically, it has a few shortcomings:&lt;/p>
&lt;ul>
&lt;li>You couldn&amp;rsquo;t pass absolute paths to the command no matter what you did.&lt;/li>
&lt;li>You couldn&amp;rsquo;t run a command per directory and pass any path to it except a dot (&lt;code>.&lt;/code>) or no path at
all.&lt;/li>
&lt;li>You couldn&amp;rsquo;t run a command once and pass file or directory paths to the command.&lt;/li>
&lt;li>You couldn&amp;rsquo;t run a command once from any directory except the project root.&lt;/li>
&lt;/ul>
&lt;p>All of these cases are addressed with the new system, which offers three command invocation keys,
&lt;code>invoke&lt;/code>, &lt;code>working_dir&lt;/code>, and &lt;code>path_args&lt;/code>. The
&lt;a href="https://github.com/houseabsolute/precious">documentation in the project&amp;rsquo;s repo&lt;/a> has been updated.
There&amp;rsquo;s also
&lt;a href="https://github.com/houseabsolute/precious/blob/master/docs/upgrade-from-0.3.0-to-0.4.0.md">documentation on upgrading to v0.4.0&lt;/a>
as well as
&lt;a href="https://github.com/houseabsolute/precious/blob/master/docs/invocation-examples.md">docs on every valid combination of invocation config options&lt;/a>.&lt;/p>
&lt;p>Please install the new release and take it for a spin. If you encounter any problems please
&lt;a href="https://github.com/houseabsolute/precious/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc">file a GitHub issue&lt;/a>.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>I do plan to add warnings in a future release and eventually remove support for the old keys.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>My Team at MongoDB is Hiring</title><link>https://blog.urth.org/2022/10/11/my-team-at-mongodb-is-hiring/</link><pubDate>Tue, 11 Oct 2022 10:23:50 -0500</pubDate><guid>https://blog.urth.org/2022/10/11/my-team-at-mongodb-is-hiring/</guid><description>&lt;p>My team at MongoDB is &lt;a href="https://grnh.se/259028f41us">hiring a senior engineer&lt;/a>. For this position you
can be 100% remote or you can choose to work from one our offices.&lt;/p>
&lt;p>I&amp;rsquo;ve been at MongoDB since May of this year and so far it&amp;rsquo;s been great. If you have questions about
the position, the team, or working at MongoDB, &lt;a href="mailto:autarch@urth.org">please reach out&lt;/a>.&lt;/p></description></item><item><title>Fixing Some Bugs in My GitHub Profile Generator</title><link>https://blog.urth.org/2022/08/14/fixing-some-bugs-in-my-github-profile-generator/</link><pubDate>Sun, 14 Aug 2022 11:28:22 -0500</pubDate><guid>https://blog.urth.org/2022/08/14/fixing-some-bugs-in-my-github-profile-generator/</guid><description>&lt;p>A while back I was looking at &lt;a href="https://github.com/autarch">the output&lt;/a> from
&lt;a href="https://github.com/autarch/autarch">my GitHub profile generator&lt;/a> and it seemed off. In particular,
the language stats seemed off. The generator sums up how many bytes of code I&amp;rsquo;ve written for each
language. and then calculates what percentage of my total output that represents.&lt;/p>
&lt;p>Here&amp;rsquo;s what it showed, more or less:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Past Two Years&lt;/th>
&lt;th>All Time&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Perl: 76%, 9.5 MB&lt;/td>
&lt;td>Perl: 77%, 11.3 MB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Rust: 21%, 2.7 MB&lt;/td>
&lt;td>Rust: 18%, 2.7 MB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Go: 2%, 214.8 KB&lt;/td>
&lt;td>Go: 2%, 368 KB&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>This isn&amp;rsquo;t &lt;em>obviously&lt;/em> wrong. I&amp;rsquo;ve written a &lt;em>lot&lt;/em> of Perl and I&amp;rsquo;ve been doing a fair bit of Rust
recently. But the Rust numbers seemed excessive. Had I written 2.7MB of Rust code in two years?
That&amp;rsquo;s a lot of code!&lt;/p>
&lt;p>So I &lt;a href="https://github.com/autarch/autarch/issues/1">filed a bug&lt;/a> to remind myself to look at this
later. Today was later.&lt;/p>
&lt;p>I added some debugging output to my code to print out various bits of info as it went, focusing on
each repo&amp;rsquo;s language stats. Eventually, I had it just print out bytes of Rust in each repo that had
any Rust. That did the trick.&lt;/p>
&lt;p>I realized that my Rust repos have &lt;em>huge&lt;/em> amounts of generated code. For example,
&lt;a href="https://github.com/houseabsolute/tailwindcss-to-rust">my &lt;code>tailwindcss-to-rust&lt;/code> project&lt;/a> exists to
generate Rust code from Tailwind CSS. The repo contains
&lt;a href="https://github.com/houseabsolute/tailwindcss-to-rust/blob/master/macros/examples/css/generated.rs">an example of that generated code&lt;/a>&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.
That generated file is 613KB all by itself.&lt;/p>
&lt;p>The fix was simple. GitHub uses &lt;a href="https://github.com/github/linguist">Linguist&lt;/a> for its language
detection and stats. You can
&lt;a href="https://github.com/github/linguist/blob/master/docs/overrides.md">set attributes in your &lt;code>.gitattributes&lt;/code> file&lt;/a>
to control how Linguist generates stats. Any file with a &lt;code>linguist-generated&lt;/code> attribute is excluded
from Linguist&amp;rsquo;s stats collection. So I went through and added this to my Rust repos.&lt;/p>
&lt;p>My Rust stat went down to 2.1MB. I&amp;rsquo;d have expected it to go down more, but I think that maybe some
of what I marked as generated was already being excluded somehow.&lt;/p>
&lt;p>And then it occurred to me that I have the same issue with some Perl repos too. Notably,
&lt;a href="https://github.com/houseabsolute/DateTime-Locale">&lt;code>DateTime-Locale&lt;/code>&lt;/a> and
&lt;a href="https://github.com/houseabsolute/DateTime-TimeZone">&lt;code>DateTime-TimeZone&lt;/code>&lt;/a> both contain ridiculous
amounts of generated code. Apparently, I knew about this Linguist thing before because
&lt;code>DateTime-Locale&lt;/code> already had a &lt;code>.gitattributes&lt;/code> file. But there was none for &lt;code>DateTime-TimeZone&lt;/code>.
Adding that removed about &lt;strong>6MB&lt;/strong> of Perl code from my stats.&lt;/p>
&lt;p>So here are the new stats:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Past Two Years&lt;/th>
&lt;th>All Time&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Perl: 60%, 3.6 MB&lt;/td>
&lt;td>Perl: 66%, 5.4 MB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Rust: 34%, 2.1 MB&lt;/td>
&lt;td>Rust: 26%, 2.1 MB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Go: 3%, 214.8 KB&lt;/td>
&lt;td>Go: 4%, 368 KB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HTML: 1%, 62.6 KB&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>That seems a bit more sensible. I&amp;rsquo;ve written a lot of Perl, but I haven&amp;rsquo;t worked on many of my Perl
projects for a while.&lt;/p>
&lt;p>I also noticed some weirdness with the count of PRs written and merged. When I run the profile
generator locally I get a higher number than when it runs in GitHub Actions. That&amp;rsquo;s presumably
because running it locally I run it with a GitHub API token that has access to private repos, so it
sees private MongoDB repos.&lt;/p>
&lt;p>But if I change the query to exclude private repos and run it locally, it gets a much &lt;em>lower&lt;/em> number
than it should. I&amp;rsquo;m not sure what&amp;rsquo;s going on here.
&lt;a href="https://github.com/pulls?q=author%3Aautarch+is%3Apr+is%3Apublic">Doing the query manually on the GitHub website&lt;/a>
I get numbers that match what the code gets in GitHub Actions, so I&amp;rsquo;m pretty sure that&amp;rsquo;s the right
one. Confusing!&lt;/p>
&lt;p>Just for good measure, I excluded all of my work-related orgs from the queries too. The point of the
profile is to highlight my FOSS work, not my work work.&lt;/p>
&lt;p>But even with this refinement I still get different results from GitHub Actions versus running it
locally. If anyone has any ideas on why, &lt;a href="mailto:autarch@urth.org">I&amp;rsquo;d love to hear them&lt;/a>!&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>GitHub is pretty slow to render this file. Be patient.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>What's the Right Way to Merge a Pull Request?</title><link>https://blog.urth.org/2022/07/02/what-s-the-right-way-to-merge-a-pull-request/</link><pubDate>Sat, 02 Jul 2022 15:58:02 -0500</pubDate><guid>https://blog.urth.org/2022/07/02/what-s-the-right-way-to-merge-a-pull-request/</guid><description>&lt;p>&lt;strong>Edit: In
&lt;a href="https://www.reddit.com/r/programming/comments/vq1p3x/whats_the_right_way_to_merge_a_pull_request/">the discussion on /r/programming&lt;/a>
a
&lt;a href="https://www.reddit.com/r/programming/comments/vq1p3x/comment/ien5oaw/?utm_source=share&amp;utm_medium=web2x&amp;context=3">comment from /u/nik9000&lt;/a>
pointed me at what I think is the best solution.&lt;/strong>&lt;/p>
&lt;p>GitHub has a feature where the PR submitter can allow me to push directly to their fork. This means
I can effectively edit their PR directly by checking it out and force pushing back to &lt;em>their&lt;/em> fork
of the repo! Apparently this has existed for a while but I didn&amp;rsquo;t notice it.&lt;/p>
&lt;p>Thanks again to &lt;a href="https://www.reddit.com/user/nik9000/">/u/nik9000&lt;/a> for pointing this out.&lt;/p>
&lt;p>So I made a new saved reply on GitHub that I will use for all future PRs I receive. Here&amp;rsquo;s the
content:&lt;/p>
&lt;aside>
&lt;small>
&lt;p>Hi, thanks for your PR! I&amp;rsquo;m pretty finicky about my projects (see
&lt;a href="https://blog.urth.org/2022/07/02/what-s-the-right-way-to-merge-a-pull-request/">this blog post&lt;/a> for
details), so I rarely merge a PR as-is. I can move forward on your PR in one of two ways:&lt;/p>
&lt;ol>
&lt;li>I check it out locally, fiddle with it as needed, merge it locally, and simply close this PR.
This will preserve at least one commit with your name on it, but the PR will show up as closed in
your GitHub stats.&lt;/li>
&lt;li>If you enable me to push directly to your fork, I can do my fiddling, then force push to your
fork and merge the resulting PR. Again, this will preserve at least one commit with your name on
it, but you &lt;em>also&lt;/em> get credit for the PR merge in your GitHub stats. The only downside is that I
will be force pushing directly to your fork.&lt;/li>
&lt;/ol>
&lt;p>Please let me know which approach you&amp;rsquo;d prefer. If I don&amp;rsquo;t hear from you before I get around to
working on this PR I&amp;rsquo;ll go with option #1.&lt;/p>
&lt;p>Thanks again for your contribution!&lt;/p>
&lt;/small>
&lt;/aside>
&lt;hr>
&lt;p>I&amp;rsquo;ve received a lot of pull requests over the years. But recently, I&amp;rsquo;ve been thinking about whether
I merge them the right way.&lt;/p>
&lt;p>When I wrote my &lt;a href="https://blog.urth.org/2022/03/28/yet-another-github-profile-generator/">my GitHub profile generator&lt;/a>, one of the stats I had it generate was
how many of my pull requests were merged. &lt;a href="https://github.com/autarch">My profile&lt;/a> currently says
I&amp;rsquo;ve created 562 PRs, of which 420&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> have been merged.&lt;/p>
&lt;p>But in fact &lt;em>more&lt;/em> than 420 have been merged in some form. It&amp;rsquo;s just that for some of them, the
maintainer fiddled a bit with the submission and merged it locally via the CLI, then closed the PR.&lt;/p>
&lt;p>My precious stats!&lt;/p>
&lt;p>The issue is that I &lt;em>always&lt;/em> do this for PRs submitted to me. I&amp;rsquo;m incredibly picky about the code in
my personal projects, so it&amp;rsquo;s nearly impossible to submit a PR that I will merge as-is. Things I
typically edit in PRs include:&lt;/p>
&lt;ul>
&lt;li>Names of everything.&lt;/li>
&lt;li>Code nits like when to include optional parentheses, exactly what operators to use when there are
multiple options, and every other possible thing you can think of.&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>&lt;/li>
&lt;li>Adding documentation for API changes/additions (people mostly forget to do this).&lt;/li>
&lt;li>Comments, including the word wrapping of comments (I like the way Emacs does it when I hit
&lt;code>alt-q&lt;/code>).&lt;/li>
&lt;li>Commit messages themselves. I like a very specific format, more or less following
&lt;a href="https://cbea.ms/git-commit/">Chris Beams&amp;rsquo;s recommendations&lt;/a>, except I&amp;rsquo;m okay with longer
subjects.&lt;/li>
&lt;li>Making sure the commits are organized well. I &lt;em>hate&lt;/em> commits like &amp;ldquo;fix typo in last commit&amp;rdquo;. Just
edit the previous commit!&lt;/li>
&lt;li>Making sure commits that make public-facing changes also update the Changes file.&lt;/li>
&lt;/ul>
&lt;p>I &lt;em>could&lt;/em> instead put the PR submitter through the wringer to do all this, but I&amp;rsquo;d rather not. The
only way to get someone else to submit something that&amp;rsquo;s exactly what I want would be to try to
operate them as a puppet via PR comments. That would be exhausting for me and infuriating for them.&lt;/p>
&lt;p>So instead what I typically do is check out the PR as a local branch with the
&lt;a href="https://cli.github.com/">GitHub CLI tool&lt;/a> (&lt;code>gh pr checkout 42&lt;/code>), fiddle with their commit(s), make
sure CI passes, and then merge it locally. This preserves their name as a committer in the git
history, so they get some credit. It&amp;rsquo;s bit weird, however, since the code with their name on it may
be fairly different from what they submitted.&lt;/p>
&lt;p>And they won&amp;rsquo;t get Internet points for the PR being merged. Hell,
&lt;a href="https://github.blog/2022-06-09-introducing-achievements-recognizing-the-many-stages-of-a-developers-coding-journey/">GitHub gives you &lt;em>achievements&lt;/em>&lt;/a>
for this stuff! So I&amp;rsquo;m sure some folks would really prefer to have a proper PR merged.&lt;/p>
&lt;p>One option I &lt;em>could&lt;/em> offer would be to take their original PR, edit it, push it back to my repo as a
new branch, then have them submit that branch as a PR. I would be open to doing this if someone
really cared about getting that &amp;ldquo;PR merged&amp;rdquo; stat up.&lt;/p>
&lt;p>So what do you think? If enough people told me they wanted this I would start offering that when
people submit a PR. Or maybe there&amp;rsquo;s another approach I haven&amp;rsquo;t though of?&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> You can
&lt;a href="mailto:autarch@urth.org">email me&lt;/a> or
&lt;a href="https://www.reddit.com/r/programming/comments/vq1p3x/whats_the_right_way_to_merge_a_pull_request/">discuss this on /r/programming&lt;/a>.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>hurr durr&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Fortunately, I&amp;rsquo;m able to suppress this when reviewing work PRs, but I channel all the insanity
back into my personal projects.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>Note that &amp;ldquo;don&amp;rsquo;t be so picky&amp;rdquo; isn&amp;rsquo;t an option.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>My Perl and Raku Conference 2022 Write-Up</title><link>https://blog.urth.org/2022/07/02/my-perl-and-raku-conference-2022-write-up/</link><pubDate>Sat, 02 Jul 2022 11:20:36 -0500</pubDate><guid>https://blog.urth.org/2022/07/02/my-perl-and-raku-conference-2022-write-up/</guid><description>&lt;p>I went to &lt;a href="https://perlconference.us/tprc-2022-hou/">The Perl and Raku Conference 2022&lt;/a> in Houston
from June 22-24. Here&amp;rsquo;s my write-up.&lt;/p>
&lt;p>Again, I&amp;rsquo;d like to thank &lt;a href="https://www.mongodb.com/">my employer, MongoDB&lt;/a>, for paying for my flight
and hotel during the conference.
&lt;a href="https://www.mongodb.com/careers/departments">We&amp;rsquo;re hiring for a variety of engineering positions&lt;/a>,
with many remote options. &lt;a href="mailto:autarch@urth.org">Contact me&lt;/a> if you have any questions about the
company or positions, and I&amp;rsquo;ll see what I can do to find out more.&lt;/p>
&lt;h2 id="the-venue-and-location">The Venue and Location&lt;/h2>
&lt;p>The conference took place at a hotel near the big Houston airport (IAH) in a neighborhood called
Greenspoint. A local friend told me it&amp;rsquo;s often called &amp;ldquo;Gunspoint&amp;rdquo;. My wife and I did not get
murdered, so that was good.&lt;/p>
&lt;p>One of the things I like to do at a conference is try all the interesting vegan food in the city. I
rented a car&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> because I knew the venue was a bit outside the core of the city, and Houston is
&lt;em>very&lt;/em> sprawly. I ended up driving more the week of the conference than I do in a month at home.
Most of the restaurants we went to were a 25+ minute drive.&lt;/p>
&lt;p>The high temperature each day was around 97-100 degrees Fahrenheit with a fair bit of humidity, so
going outside was like getting punched in the face by the sun.&lt;/p>
&lt;p>The hotel itself was fine but they had the air conditioning dialed up to 11. So in some conference
rooms, I had to wear jeans and a long-sleeved shirt to feel comfortable. Meanwhile, the best
clothing for outdoors was &amp;hellip; there was no good clothing for going outdoors.&lt;/p>
&lt;p>There was some sort of issue with the projection in many rooms that seemed to make slides harder to
read than usual. I think maybe the bulbs in the projectors needed to be replaced? Or maybe we needed
to dim the lighting in the rooms?&lt;/p>
&lt;h2 id="the-talks">The Talks&lt;/h2>
&lt;p>Here are some write-ups of many (but not all) of the talks I attended.&lt;/p>
&lt;h3 id="people-still-use-perl---twenty-years-of-making-a-living-with-a-dead-language---ruth-holloway">People Still Use Perl? - Twenty Years of Making a Living with a Dead Language - Ruth Holloway&lt;/h3>
&lt;p>&lt;a href="https://youtu.be/XnDkpqwzpp0">Watch it on YouTube&lt;/a>.&lt;/p>
&lt;p>This is a talk about Ruth&amp;rsquo;s history with Perl. It&amp;rsquo;s not super technical, but it&amp;rsquo;s pretty
interesting. It&amp;rsquo;s also really personal and emotional, which isn&amp;rsquo;t something you see a lot at
programming conferences. I&amp;rsquo;m impressed that Ruth was able to be so open about herself!&lt;/p>
&lt;h3 id="newfangled-bringing-newrelic-to-perl-with-alien-and-ffi-technology---graham-ollis">NewFangled: Bringing NewRelic to Perl with Alien and FFI Technology - Graham Ollis&lt;/h3>
&lt;p>&lt;a href="https://youtu.be/6o_w-xxjbbg">Watch it on YouTube&lt;/a>.&lt;/p>
&lt;p>Graham has created several CPAN modules to make FFI in Perl easier than it is with raw XS, including
&lt;a href="https://metacpan.org/dist/FFI-Platypus">FFI-Platypus&lt;/a>, which lets you implement FFI entirely in
Perl.&lt;/p>
&lt;p>I got to this talk a little late but the parts I caught were interesting, and mostly covered the API
of this module and some other related ones like &lt;a href="https://metacpan.org/dist/FFI-C">FFI-C&lt;/a>.&lt;/p>
&lt;p>This talk is probably &lt;em>mostly&lt;/em> interesting to people interested in Perl, unless you&amp;rsquo;re implementing
FFI tools for another language, in which case there&amp;rsquo;s a lot to learn from here.&lt;/p>
&lt;h3 id="taming-the-unicode-beast---felipe-gasper">Taming the Unicode Beast - Felipe Gasper&lt;/h3>
&lt;p>&lt;a href="https://youtu.be/yH5IyYyvWHU">Watch it on YouTube&lt;/a>.&lt;/p>
&lt;p>This is a good talk for anyone interested in handling Unicode properly. Felipe did a good job of
clarifying the difference between Unicode and UTF-8, characters versus bytes, and all the usual
confusing parts of Unicode.&lt;/p>
&lt;p>He made a good argument that the tools built into the Perl core aren&amp;rsquo;t good enough for proper UTF-8
handling and that you should use &lt;a href="https://metacpan.org/pod/Sys::Binmode">his Sys::Binmode module&lt;/a> to
fix it. He also explained a variety of potential UTF-8 gotchas in both Perl and XS code.&lt;/p>
&lt;p>A lot of these issues stem from the fact that Perl does not distinguish between a scalar containing
characters and one containing bytes at a type level. This should sound familiar since this is the
issue that led to Python 3. So I guess that&amp;rsquo;s what we&amp;rsquo;ll get in Perl 7?&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>&lt;/p>
&lt;p>I think this will be of interest to anyone interested in Unicode issues. And if you&amp;rsquo;re designing
your own language, you should learn about this stuff!&lt;/p>
&lt;h3 id="a-nailgun-for-raku---daniel-sockwell">A Nailgun for Raku - Daniel Sockwell&lt;/h3>
&lt;p>&lt;a href="https://youtu.be/NT1uBYBubNU">Watch it on YouTube&lt;/a>.&lt;/p>
&lt;p>A lightning talk about improving the startup time for Raku scripts.&lt;/p>
&lt;p>Fun fact, the solution that Daniel discusses was implemented for &lt;em>Perl&lt;/em> back in the early 2000s.
Check out &lt;a href="https://metacpan.org/dist/PPerl">Matt Sergeant&amp;rsquo;s PPerl&lt;/a>.&lt;/p>
&lt;p>Everything old is new again.&lt;/p>
&lt;h3 id="open-source-self-hosted-password-management-with-bitwarden--vaultwarden---daniel-sockwell">Open Source, Self Hosted Password Management with Bitwarden + Vaultwarden - Daniel Sockwell&lt;/h3>
&lt;p>&lt;a href="https://youtu.be/hBRg-S4YyUk">Watch it on YouTube&lt;/a>.&lt;/p>
&lt;p>I hope you&amp;rsquo;re all using a password manager. If not, you should be.&lt;/p>
&lt;p>But did you know you can host your &lt;em>own&lt;/em> password manager sync server? But I&amp;rsquo;ll stick with 1Password
because I&amp;rsquo;m lazy.&lt;/p>
&lt;h3 id="modern-approaches-to-ancient-perls---brian-kelly">Modern Approaches to Ancient Perls - Brian Kelly&lt;/h3>
&lt;p>&lt;a href="https://youtu.be/CwHRr_NTVjk">Watch it on YouTube&lt;/a>.&lt;/p>
&lt;p>I think I went to this partly because of the schadenfreude. I haven&amp;rsquo;t done much Perl professionally
since 2017 or so, and I was always able to use a modern Perl, so this hasn&amp;rsquo;t been a problem for me.&lt;/p>
&lt;p>But it was interesting to learn how to make older Perls less painful to use.&lt;/p>
&lt;h3 id="command-line-filters---time-to-shine---bruce-gray">Command-line Filters - Time to Shine - Bruce Gray&lt;/h3>
&lt;p>The recording for this one didn&amp;rsquo;t come out, so I can&amp;rsquo;t link to it. I&amp;rsquo;ve heard a re-recording may
happen so I will update this post if that happens and I notice the update.&lt;/p>
&lt;p>This talk covered how to use command-line filters. I already knew a fair bit about this but I still
learned some new things.&lt;/p>
&lt;h3 id="three-ways-to-make-wrong-code-look-wrong-er---daniel-sockwell">Three Ways to Make Wrong Code Look Wrong (er) - Daniel Sockwell&lt;/h3>
&lt;p>&lt;a href="https://youtu.be/k-4agG8aJ1k">Watch it on YouTube&lt;/a>.&lt;/p>
&lt;p>I really liked this talk. It&amp;rsquo;s about three different approaches to making developer mistakes
obvious. I preferred the version using the type system, and this approach can be used by any
language with a sufficiently not-terrible type system (I&amp;rsquo;m looking at you, C).&lt;/p>
&lt;h3 id="why-do-programmers-love-rust---dave-rolsky">Why Do Programmers Love Rust? - Dave Rolsky&lt;/h3>
&lt;p>&lt;a href="https://youtu.be/vEuG2YoJZJw">Watch it on YouTube&lt;/a>.&lt;/p>
&lt;blockquote>
&lt;p>A heartbreaking work of staggering genius. Nothing will ever be the same. &lt;cite>Anonymous
Attendee&lt;/cite>&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>I was floored! Then I was ceilinged and walled too! &lt;cite>Anonymous Attendee&lt;/cite>&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>He is clearly making these quotes up. &lt;cite>Anonymous Attendee&lt;/cite>&lt;/p>
&lt;/blockquote>
&lt;p>I think this talk went reasonably well. I started rushing a bit towards the end because I was afraid
I would run out of time, and then I ended a little early. Doh! I did practice this at home to check
the timing, but for some reason, in the moment I felt like I was behind schedule. Next time I give
this I&amp;rsquo;ll slow down a little (assuming I have a 50 minute slot).&lt;/p>
&lt;p>Also, a few slides were hard to read. For the code examples, I realize now that it&amp;rsquo;d be better to
use a light theme and to make sure that comments aren&amp;rsquo;t very dim as they are in the theme I used. So
I switched the slides to the highlight.js VS theme, which looks much better.&lt;/p>
&lt;p>For the screenshots of compiler errors, I&amp;rsquo;m not sure how to fix these. I suspect the ideal approach
is to not use screenshots but to instead recreate the error in the browser in a large font. But
that&amp;rsquo;s a lot of work.&lt;/p>
&lt;p>A little bit of this was due to the projector issues I mentioned above, but mostly it&amp;rsquo;s on me to
make better slides in the future.&lt;/p>
&lt;p>However, looking at the video it&amp;rsquo;s all very readable so that&amp;rsquo;s nice.&lt;/p>
&lt;p>And wow, I move my hands a &lt;em>lot&lt;/em> when I&amp;rsquo;m giving a talk. Okay, time to stop watching this video of
myself.&lt;/p>
&lt;h3 id="meet-the-tpf-board">Meet the TPF Board&lt;/h3>
&lt;p>&lt;a href="https://youtu.be/GSyMlEaaJro">Watch it on YouTube&lt;/a>.&lt;/p>
&lt;p>This was a brief presentation about The Perl Foundation/Raku Foundation, followed by Q&amp;amp;A. If you
wonder what the foundation does, this may help answer your questions.&lt;/p>
&lt;h3 id="the-perl-navigator-code-intelligence-for-any-editor---brian-scannell">The Perl Navigator: Code Intelligence for any Editor - Brian Scannell&lt;/h3>
&lt;p>&lt;a href="https://youtu.be/ENN05EhBkgM">Watch it on YouTube&lt;/a>.&lt;/p>
&lt;p>Last time I looked for an LSP server for Perl I gave up. The ones I tried didn&amp;rsquo;t seem to work.&lt;/p>
&lt;p>But then the day before the conference proper, some folks were talking about Perl Navigator in the
conference Slack, so I gave it a shot. And to my surprise, it worked quite well out of the box! And
Brian fixed the one notable issue I had (not including &lt;code>./lib&lt;/code> in the module search path) quickly.&lt;/p>
&lt;p>So I highly recommend Perl Navigator if you want an LSP server. &lt;strong>And if you&amp;rsquo;ve never used an LSP
server, you want an LSP server&lt;/strong>. Using &lt;code>lsp-mode&lt;/code> in Emacs has greatly improved my productivity. I
don&amp;rsquo;t know why I took so long to start using it.&lt;/p>
&lt;p>So this talk is both about LSP in general and the implementation for Perl specifically. I think it
will be of interest to anyone interested in LSP.&lt;/p>
&lt;h3 id="mastering-english-in-perl---makoto-nozaki">Mastering English in Perl - Makoto Nozaki&lt;/h3>
&lt;p>&lt;a href="https://youtu.be/TLZwWNbl2wI">Watch it on YouTube&lt;/a>.&lt;/p>
&lt;p>By far the funniest and cutest talk at the conference. Watch it now!&lt;/p>
&lt;h3 id="ipv4-subnetting-for-humans---teddy-vandenberg">IPv4 subnetting for humans - Teddy Vandenberg&lt;/h3>
&lt;p>&lt;a href="https://youtu.be/OGVAlPBiNYM">Watch it on YouTube&lt;/a>.&lt;/p>
&lt;p>He says the math is simple but my brain still hurt. This will be useful for people who are smarter
than me.&lt;/p>
&lt;h3 id="cli-tools-i-use---dave-rolsky">CLI Tools I Use - Dave Rolsky&lt;/h3>
&lt;p>&lt;a href="https://youtu.be/A5NZIw3jp2E">Watch it on YouTube&lt;/a>&lt;/p>
&lt;p>It&amp;rsquo;s me again. Just a quick talk on some CLI tools I use that might be of interest. This talk has no
Perl content and should be of interest to anyone who uses the CLI.&lt;/p>
&lt;p>This was inspired by Bruce Gray&amp;rsquo;s talk earlier that day on command-line filters&lt;/p>
&lt;h3 id="sqlabstract---caveat-emptor---dimitrios-kechagias">SQL::Abstract - Caveat Emptor - Dimitrios Kechagias&lt;/h3>
&lt;p>&lt;a href="https://youtu.be/oHUIbWjphnU">Watch it on YouTube&lt;/a>&lt;/p>
&lt;p>I was never a fan of &lt;a href="https://metacpan.org/pod/SQL::Abstract">&lt;code>SQL::Abstract&lt;/code>&lt;/a> because I find its
API arbitrary and confusing. But apparently it&amp;rsquo;s also really slow.&lt;/p>
&lt;h3 id="dispatches-from-raku---daniel-sockwell">Dispatches from Raku - Daniel Sockwell&lt;/h3>
&lt;p>&lt;a href="https://youtu.be/TNZQuzvroEY">Watch it on YouTube&lt;/a>&lt;/p>
&lt;p>A quick summary of what&amp;rsquo;s new in Raku over the past year or so.&lt;/p>
&lt;h3 id="advice-for-presenters">Advice for Presenters&lt;/h3>
&lt;p>Hey, you, Presenter! Have you tried reading your slides on a projector from thirty feet away? No,
you clearly haven&amp;rsquo;t, because I can&amp;rsquo;t read your slides and I&amp;rsquo;m closer than that!&lt;/p>
&lt;p>Here are some things to consider &amp;hellip;&lt;/p>
&lt;p>More than 10 lines of code on a slide is probably too much. You can get away with more lines if you
use highlighting to just focus on a few lines, but be very aware of the font size.&lt;/p>
&lt;p>Grey on grey is not readable. Blue on grey is not readable. Grey on blue is not readable. Many other
color combinations are not readable!&lt;/p>
&lt;p>Use high-contrast colors. Use higher contrast than you&amp;rsquo;d use for a web page. Remember, your audience
may not be sitting up close and your projector probably will not render things as brightly as your
laptop screen.&lt;/p>
&lt;p>Less is more. Less text, less code, fewer boxes, fewer graphics. The more stuff you put on a slide
the smaller that stuff is and the harder it is to read. Instead of one busy slide, make four, five,
or ten simple slides!&lt;/p>
&lt;h2 id="non-conference-stuff">Non-Conference Stuff&lt;/h2>
&lt;p>We stayed for a few extra days on either side of the conference so my wife and I could do some
tourist stuff. On the Monday before the conference, we went to the Johnson Space Center, which was
fun. I enjoyed walking through the 747 that flew the space shuttle around.&lt;/p>
&lt;p>The day after the conference, I met up with a former coworker from ActiveState and his wife. We had
lunch with them, then went to the art museum to see an M.C. Escher exhibition. This was great! They
had lots of original prints as well as the wood he would carve to make them. They also had drafts
and work from his planning process. The amount of work involved in producing these is amazing. My
wife and I greatly enjoyed hanging out with them and seeing the exhibition.&lt;/p>
&lt;p>Of the food we tried, I think the best was &lt;a href="https://trendyhouston.com/">Trendy Vegan&lt;/a>, which serves
vegan Chinese. The salt and pepper tofu was fantastic. I also liked
&lt;a href="https://www.tainanbistrohouston.com/">Tainan Bistro&lt;/a>, which serves Taiwanese food. But I&amp;rsquo;m not sure
how easy it would be to get vegan food there if you don&amp;rsquo;t speak Mandarin.&lt;/p>
&lt;h2 id="the-hallway-track">The Hallway Track&lt;/h2>
&lt;p>This is the best part of the conference. I enjoyed seeing old friends and making new ones. I was
bummed that some folks couldn&amp;rsquo;t make it for various reasons, including visa and passport issues. I&amp;rsquo;m
hopeful that they&amp;rsquo;ll be able to come next year.&lt;/p>
&lt;p>I was quite happy that for the first time, someone with prior climbing experience joined me for my
Rock Climbing BOF! He knew how to belay so I could do more than climb the autobelays. But it would
have been nice to have more folks join us. Hopefully next year I&amp;rsquo;ll get a few more takers.&lt;/p>
&lt;h2 id="next-years-conference">Next Year&amp;rsquo;s Conference&lt;/h2>
&lt;p>It will be in Toronto! The 2005 conference was in Toronto and I loved it. The weather was great and
the city is &lt;em>very&lt;/em> walkable, which is a huge plus for me. I didn&amp;rsquo;t like driving so much in Houston.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>I paid for this out of pocket, since it was purely for entertainment purposes.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>This is a joke.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Job Search 2022 Update: Postscript</title><link>https://blog.urth.org/2022/06/17/job-search-2022-update-postscript/</link><pubDate>Fri, 17 Jun 2022 11:10:27 -0500</pubDate><guid>https://blog.urth.org/2022/06/17/job-search-2022-update-postscript/</guid><description>&lt;p>I&amp;rsquo;ve been at &lt;a href="https://www.mongodb.com/">MongoDB&lt;/a> for six weeks now and it&amp;rsquo;s been great. BTW, we&amp;rsquo;re
hiring and we have a lot of
&lt;a href="https://www.mongodb.com/careers/departments/engineering#openings">remote engineering positions&lt;/a>&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.
Please &lt;a href="mailto:autarch@urth.org">email me&lt;/a> if you have questions about working at MongoDB or about
specific positions&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>But that&amp;rsquo;s not why I&amp;rsquo;m writing this post. No, I&amp;rsquo;m writing because apparently my job search wasn&amp;rsquo;t
quite done.&lt;/p>
&lt;p>Today. Today! &lt;strong>TODAY!&lt;/strong> I got an email from GitHub saying that the position I applied for had been
filled:&lt;/p>
&lt;blockquote>
&lt;p>The role that you originally applied for has now been filled unfortunately. However, GitHub is
continuing to build our team and we are still hiring for a number of roles. Please feel free to
check out our Careers Page and see if there are any positions that look appropriate for you and
please apply if so!&lt;/p>
&lt;/blockquote>
&lt;p>Yes, I&amp;rsquo;m definitely going to apply for another role. After all, it took them just 99 days to respond
to my first application. I should apply again. Maybe I&amp;rsquo;ll hear back before I retire.&lt;/p>
&lt;p>Also, back on May 16, I heard from Oso, who look like a hare next to GitHub&amp;rsquo;s tortoise. It took them
just 67 days.&lt;/p>
&lt;p>I really don&amp;rsquo;t understand what&amp;rsquo;s going on at either of these places.&lt;/p>
&lt;p>Of course, both of them have Netflix beat, who still haven&amp;rsquo;t responded at all.&lt;/p>
&lt;p>Tech hiring. It&amp;rsquo;s ridiculous.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Click on the &amp;ldquo;All locations&amp;rdquo; dropdown and you&amp;rsquo;ll see various remote options.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>No promises on whether I can answer those questions, but I can try.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Restoring Window Positions in GNOME After Switching Monitor Inputs</title><link>https://blog.urth.org/2022/05/14/restoring-window-positions-in-gnome-after-switching-monitor-inputs/</link><pubDate>Sat, 14 May 2022 14:05:58 -0500</pubDate><guid>https://blog.urth.org/2022/05/14/restoring-window-positions-in-gnome-after-switching-monitor-inputs/</guid><description>&lt;p>I suspect that this title makes no sense to most people, so here&amp;rsquo;s the background.&lt;/p>
&lt;p>Like most normal people, I have four&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> computers in my office. I used to have three, but that was
shameful, so I was very relieved to get a new laptop for my new job at
&lt;a href="https://www.mongodb.com/">MongoDB&lt;/a>.&lt;/p>
&lt;p>A while back, I bought
&lt;a href="https://smile.amazon.com/gp/product/B083JKDNRJ">a USB switching device with a remote&lt;/a>. This
eliminated the need to physically switch my USB hub&amp;rsquo;s cable from one computer to another.&lt;/p>
&lt;p>I have two monitors connected to these computers, and I switch between inputs on the monitors when I
switch computers. I used to do this manually by using the buttons on the monitors, but this was
annoying. I&amp;rsquo;ve used KVM switches before but my experience has been that they&amp;rsquo;re all junk, so I
didn&amp;rsquo;t want to go that route again.&lt;/p>
&lt;p>Fortunately, I found an
&lt;a href="https://github.com/haimgel/display-switch">awesome project in Rust called display-switch&lt;/a> created
by &lt;a href="https://github.com/haimgel">Haim Gelfenbeyn&lt;/a>. It runs on Linux, macOS, and Windows as a
background service. It listens for USB connect/disconnect events and then uses
&lt;a href="https://en.wikipedia.org/wiki/Display_Data_Channel">DDC&lt;/a> commands to switch the inputs on the
monitor. With this configured on each computer, I can use the USB switch&amp;rsquo;s remote to switch all the
USB devices &lt;em>and&lt;/em> the monitors together. It&amp;rsquo;s great!&lt;/p>
&lt;p>And for a while, everything worked fine. I&amp;rsquo;d switch to my Windows computer for gaming, then back to
Linux for day-to-day work and computing. But for some reason when I added my work laptop to the mix,
something went wrong on my personal Linux desktop.&lt;/p>
&lt;p>Suddenly, when the monitors switched,
&lt;a href="https://en.wikipedia.org/wiki/Mutter_(software)">mutter&lt;/a>&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> would move all the windows on my
left monitor onto the right monitor. This was very, very annoying.&lt;/p>
&lt;p>Surely, I thought, there must be a way to fix this. The actual issue has been discussed in various
forums for quite a few years. Here&amp;rsquo;s
&lt;a href="https://gitlab.gnome.org/GNOME/mutter/-/issues/1419">a bug report for mutter on the topic&lt;/a>, which
has links to more bugs for Red Hat, Ubuntu, and gnome-shell.&lt;/p>
&lt;p>I don&amp;rsquo;t think this had anything to do with my work laptop, exactly. Instead, it&amp;rsquo;s probably because I
shifted some cabling around when I added my work laptop to the mix, moving my personal Linux desktop
from HDMI1 to DisplayPort2 on my left monitor. This in turn changes the timing of when the monitor
sleeps and wakes when the input is switched, and mutter reacts by moving all my windows around.&lt;/p>
&lt;p>The &lt;code>display-switch&lt;/code> project lets you run arbitrary commands when the USB device disconnects and
connects. I wanted to use this to keep my windows where I put them.&lt;/p>
&lt;p>In reading about the issue, I found some workarounds people had come up with, including a very
creative one using &lt;a href="https://linux.die.net/man/1/wmctrl">&lt;code>wmctrl&lt;/code>&lt;/a>. But &lt;code>wmctrl&lt;/code> only works with X
and X is going away in favor of Wayland.&lt;/p>
&lt;p>But then I read some more and discovered that Gnome has a
&lt;a href="https://gjs.guide/">comprehensive JavaScript binding&lt;/a> that you can invoke with some &lt;code>dbus&lt;/code> magic:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$&amp;gt; gdbus call \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --session \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --dest org.gnome.Shell \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --object-path /org/gnome/Shell \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --method org.gnome.Shell.Eval \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;some_js_stuff(); and_more();&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Could I use this to somehow save and restore my windows? Yes, I could! When you run this command,
you will get some output to &lt;code>stdout&lt;/code> like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$&amp;gt; gdbus call \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --session \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --dest org.gnome.Shell \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --object-path /org/gnome/Shell \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --method org.gnome.Shell.Eval \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;42&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(true, &amp;#39;42&amp;#39;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$&amp;gt; gdbus call \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --session \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --dest org.gnome.Shell \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --object-path /org/gnome/Shell \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --method org.gnome.Shell.Eval \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#39;throw &amp;#34;Foo&amp;#34;&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(false, &amp;#39;Foo&amp;#39;)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The output is a list where the first item is a boolean indicating whether the code threw an error (I
think), and the second is the error output &lt;em>or&lt;/em> the value of the last statement executed.&lt;/p>
&lt;p>So I wrote a little Perl script to execute the JS I needed and parse the output to check if it
worked.&lt;/p>
&lt;p>Here&amp;rsquo;s the code in full:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt"> 10
&lt;/span>&lt;span class="lnt"> 11
&lt;/span>&lt;span class="lnt"> 12
&lt;/span>&lt;span class="lnt"> 13
&lt;/span>&lt;span class="lnt"> 14
&lt;/span>&lt;span class="lnt"> 15
&lt;/span>&lt;span class="lnt"> 16
&lt;/span>&lt;span class="lnt"> 17
&lt;/span>&lt;span class="lnt"> 18
&lt;/span>&lt;span class="lnt"> 19
&lt;/span>&lt;span class="lnt"> 20
&lt;/span>&lt;span class="lnt"> 21
&lt;/span>&lt;span class="lnt"> 22
&lt;/span>&lt;span class="lnt"> 23
&lt;/span>&lt;span class="lnt"> 24
&lt;/span>&lt;span class="lnt"> 25
&lt;/span>&lt;span class="lnt"> 26
&lt;/span>&lt;span class="lnt"> 27
&lt;/span>&lt;span class="lnt"> 28
&lt;/span>&lt;span class="lnt"> 29
&lt;/span>&lt;span class="lnt"> 30
&lt;/span>&lt;span class="lnt"> 31
&lt;/span>&lt;span class="lnt"> 32
&lt;/span>&lt;span class="lnt"> 33
&lt;/span>&lt;span class="lnt"> 34
&lt;/span>&lt;span class="lnt"> 35
&lt;/span>&lt;span class="lnt"> 36
&lt;/span>&lt;span class="lnt"> 37
&lt;/span>&lt;span class="lnt"> 38
&lt;/span>&lt;span class="lnt"> 39
&lt;/span>&lt;span class="lnt"> 40
&lt;/span>&lt;span class="lnt"> 41
&lt;/span>&lt;span class="lnt"> 42
&lt;/span>&lt;span class="lnt"> 43
&lt;/span>&lt;span class="lnt"> 44
&lt;/span>&lt;span class="lnt"> 45
&lt;/span>&lt;span class="lnt"> 46
&lt;/span>&lt;span class="lnt"> 47
&lt;/span>&lt;span class="lnt"> 48
&lt;/span>&lt;span class="lnt"> 49
&lt;/span>&lt;span class="lnt"> 50
&lt;/span>&lt;span class="lnt"> 51
&lt;/span>&lt;span class="lnt"> 52
&lt;/span>&lt;span class="lnt"> 53
&lt;/span>&lt;span class="lnt"> 54
&lt;/span>&lt;span class="lnt"> 55
&lt;/span>&lt;span class="lnt"> 56
&lt;/span>&lt;span class="lnt"> 57
&lt;/span>&lt;span class="lnt"> 58
&lt;/span>&lt;span class="lnt"> 59
&lt;/span>&lt;span class="lnt"> 60
&lt;/span>&lt;span class="lnt"> 61
&lt;/span>&lt;span class="lnt"> 62
&lt;/span>&lt;span class="lnt"> 63
&lt;/span>&lt;span class="lnt"> 64
&lt;/span>&lt;span class="lnt"> 65
&lt;/span>&lt;span class="lnt"> 66
&lt;/span>&lt;span class="lnt"> 67
&lt;/span>&lt;span class="lnt"> 68
&lt;/span>&lt;span class="lnt"> 69
&lt;/span>&lt;span class="lnt"> 70
&lt;/span>&lt;span class="lnt"> 71
&lt;/span>&lt;span class="lnt"> 72
&lt;/span>&lt;span class="lnt"> 73
&lt;/span>&lt;span class="lnt"> 74
&lt;/span>&lt;span class="lnt"> 75
&lt;/span>&lt;span class="lnt"> 76
&lt;/span>&lt;span class="lnt"> 77
&lt;/span>&lt;span class="lnt"> 78
&lt;/span>&lt;span class="lnt"> 79
&lt;/span>&lt;span class="lnt"> 80
&lt;/span>&lt;span class="lnt"> 81
&lt;/span>&lt;span class="lnt"> 82
&lt;/span>&lt;span class="lnt"> 83
&lt;/span>&lt;span class="lnt"> 84
&lt;/span>&lt;span class="lnt"> 85
&lt;/span>&lt;span class="lnt"> 86
&lt;/span>&lt;span class="lnt"> 87
&lt;/span>&lt;span class="lnt"> 88
&lt;/span>&lt;span class="lnt"> 89
&lt;/span>&lt;span class="lnt"> 90
&lt;/span>&lt;span class="lnt"> 91
&lt;/span>&lt;span class="lnt"> 92
&lt;/span>&lt;span class="lnt"> 93
&lt;/span>&lt;span class="lnt"> 94
&lt;/span>&lt;span class="lnt"> 95
&lt;/span>&lt;span class="lnt"> 96
&lt;/span>&lt;span class="lnt"> 97
&lt;/span>&lt;span class="lnt"> 98
&lt;/span>&lt;span class="lnt"> 99
&lt;/span>&lt;span class="lnt">100
&lt;/span>&lt;span class="lnt">101
&lt;/span>&lt;span class="lnt">102
&lt;/span>&lt;span class="lnt">103
&lt;/span>&lt;span class="lnt">104
&lt;/span>&lt;span class="lnt">105
&lt;/span>&lt;span class="lnt">106
&lt;/span>&lt;span class="lnt">107
&lt;/span>&lt;span class="lnt">108
&lt;/span>&lt;span class="lnt">109
&lt;/span>&lt;span class="lnt">110
&lt;/span>&lt;span class="lnt">111
&lt;/span>&lt;span class="lnt">112
&lt;/span>&lt;span class="lnt">113
&lt;/span>&lt;span class="lnt">114
&lt;/span>&lt;span class="lnt">115
&lt;/span>&lt;span class="lnt">116
&lt;/span>&lt;span class="lnt">117
&lt;/span>&lt;span class="lnt">118
&lt;/span>&lt;span class="lnt">119
&lt;/span>&lt;span class="lnt">120
&lt;/span>&lt;span class="lnt">121
&lt;/span>&lt;span class="lnt">122
&lt;/span>&lt;span class="lnt">123
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-perl" data-lang="perl">&lt;span class="line">&lt;span class="cl">&lt;span class="ch">#!/usr/bin/env perl&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">use&lt;/span> &lt;span class="nn">v5&lt;/span>&lt;span class="mf">.32&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">use&lt;/span> &lt;span class="nn">strict&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">use&lt;/span> &lt;span class="nn">warnings&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">use&lt;/span> &lt;span class="nn">autodie&lt;/span> &lt;span class="sx">qw( :all )&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">use&lt;/span> &lt;span class="nn">Capture::Tiny&lt;/span> &lt;span class="sx">qw( capture_stdout )&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">use&lt;/span> &lt;span class="nn">JSON::MaybeXS&lt;/span> &lt;span class="sx">qw( decode_json encode_json )&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">use&lt;/span> &lt;span class="nn">Path::Tiny&lt;/span> &lt;span class="sx">qw( path )&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">my&lt;/span> &lt;span class="nv">$POSITION_FILE&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">=&lt;/span> &lt;span class="n">path&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#39;/home/autarch/.config/display-switch/window-positions.json&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">sub&lt;/span> &lt;span class="nf">main&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span> &lt;span class="nv">@ARGV&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nv">$ARGV&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="ow">eq&lt;/span> &lt;span class="s">&amp;#39;restore&amp;#39;&lt;/span> &lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">restore&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elsif&lt;/span> &lt;span class="p">(&lt;/span> &lt;span class="nv">@ARGV&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nv">$ARGV&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="ow">eq&lt;/span> &lt;span class="s">&amp;#39;save&amp;#39;&lt;/span> &lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">save&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">die&lt;/span> &lt;span class="sx">q{You must specify &amp;#39;save&amp;#39; or &amp;#39;restore&amp;#39; as an argument&amp;#39;}&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">my&lt;/span> &lt;span class="nv">$SAVE_JS&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;&lt;/span>&lt;span class="dl">EOF&lt;/span>&lt;span class="s">&amp;#39;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">const { Gio, GLib } = imports.gi;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">let windows = {};
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">global.get_window_actors().forEach(function (window) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> let mw = window.meta_window;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> let rect = mw.get_frame_rect();
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> let title = mw.get_title();
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if (title === null || title === &amp;#34;gnome-shell&amp;#34;) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> return;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> let id = mw.get_id();
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> let w = {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> title: title,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> monitor: mw.get_monitor(),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> x: rect.x,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> y: rect.y,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> w: rect.width,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> h: rect.height,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> };
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> windows[id] = w;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">});
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">const filepath = GLib.build_filenamev([
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> GLib.get_home_dir(),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;.config&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;display-switch&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;window-positions.json&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">]);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">const file = Gio.File.new_for_path(filepath);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">const [ok] = file.replace_contents(
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> JSON.stringify(windows),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> null,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> false,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> Gio.FileCreateFlags.REPLACE_DESTINATION,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> null
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">if (!ok) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> log(&amp;#34;Could not write to file at &amp;#34; + filepath);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&lt;/span>&lt;span class="dl">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">sub&lt;/span> &lt;span class="nf">save&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">run_js&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$SAVE_JS&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">my&lt;/span> &lt;span class="nv">$RESTORE_JS&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;&lt;/span>&lt;span class="dl">EOF&lt;/span>&lt;span class="s">&amp;#39;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">const { Gio, GLib } = imports.gi;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">const filepath = GLib.build_filenamev([
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> GLib.get_home_dir(),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;.config&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;display-switch&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;window-positions.json&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">]);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">const file = Gio.File.new_for_path(filepath);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">const [ok, contents] = file.load_contents(null);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">if (!ok) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> log(&amp;#34;Could not read from file at &amp;#34; + filepath);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">const windows = JSON.parse(contents.toString());
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">global.get_window_actors().forEach(function (window) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> let mw = window.meta_window;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> let rect = mw.get_frame_rect();
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> let id = mw.get_id();
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> let w = windows[id];
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if (w === null || w === undefined) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> return;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mw.move_to_monitor(w.monitor);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mw.move_resize_frame(true, w.x, w.y, w.w, w.h);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">});
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&lt;/span>&lt;span class="dl">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">sub&lt;/span> &lt;span class="nf">restore&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># waiting for the monitor to be active again.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">sleep&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">run_js&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$RESTORE_JS&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">sub&lt;/span> &lt;span class="nf">run_js&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">my&lt;/span> &lt;span class="nv">$js&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">shift&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">my&lt;/span> &lt;span class="nv">@command&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sx">qw( gdbus call)&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#39;--session&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="sx">qw( --dest org.gnome.Shell )&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sx">qw( --object-path /org/gnome/Shell )&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sx">qw( --method org.gnome.Shell.Eval)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">my&lt;/span> &lt;span class="nv">$stdout&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">capture_stdout&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">sub&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">system&lt;/span>&lt;span class="p">(&lt;/span> &lt;span class="nv">@command&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nv">$js&lt;/span> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">$stdout&lt;/span> &lt;span class="o">=~&lt;/span> &lt;span class="sr">s/^\(|\)$//g&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">my&lt;/span> &lt;span class="p">(&lt;/span> &lt;span class="nv">$ok&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nv">$err&lt;/span> &lt;span class="p">)&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">split&lt;/span> &lt;span class="sr">/\s*,\s*/&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nv">$stdout&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">die&lt;/span> &lt;span class="s">&amp;#34;Error running GJS: $err&amp;#34;&lt;/span> &lt;span class="k">unless&lt;/span> &lt;span class="nv">$ok&lt;/span> &lt;span class="ow">eq&lt;/span> &lt;span class="s">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">main&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The Perl parts aren&amp;rsquo;t that interesting. It&amp;rsquo;s the JS that&amp;rsquo;s doing all the work. Here&amp;rsquo;s the code to
save the window positions:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">Gio&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">GLib&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">imports&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">gi&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">let&lt;/span> &lt;span class="nx">windows&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">global&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get_window_actors&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">window&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">mw&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">meta_window&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">rect&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">mw&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get_frame_rect&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">title&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">mw&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get_title&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">title&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="kc">null&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">title&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s2">&amp;#34;gnome-shell&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">mw&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get_id&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">w&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">title&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">title&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">monitor&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">mw&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get_monitor&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">x&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">rect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">y&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">rect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">y&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">rect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">width&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">h&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">rect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">height&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">windows&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">w&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">filepath&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">GLib&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">build_filenamev&lt;/span>&lt;span class="p">([&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">GLib&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get_home_dir&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;.config&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;display-switch&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;window-positions.json&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">file&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">Gio&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">File&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">new_for_path&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filepath&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nx">ok&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">file&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">replace_contents&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">JSON&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">stringify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">windows&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Gio&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">FileCreateFlags&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">REPLACE_DESTINATION&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">ok&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Could not write to file at &amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">filepath&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This loops through all the windows and records information for each window. It saves the monitor the
window is on, its unique ID, its X &amp;amp; Y position, and its height &amp;amp; width. This gets written as JSON
to a file every time the USB device is disconnected.&lt;/p>
&lt;p>One odd thing is that &lt;code>global.get_window_actors()&lt;/code> includes one window with a &lt;code>null&lt;/code> title and
another window for the &lt;code>gnome-shell&lt;/code> process. I&amp;rsquo;m not sure what that &lt;code>null&lt;/code> title window is, but
it&amp;rsquo;s best to just skip it and &lt;code>gnome-shell&lt;/code>.&lt;/p>
&lt;p>The restore code is even simpler:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">Gio&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">GLib&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">imports&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">gi&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">filepath&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">GLib&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">build_filenamev&lt;/span>&lt;span class="p">([&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">GLib&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get_home_dir&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;.config&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;display-switch&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;window-positions.json&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">file&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">Gio&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">File&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">new_for_path&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filepath&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nx">ok&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">contents&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">file&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">load_contents&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">ok&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Could not read from file at &amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">filepath&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">windows&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">JSON&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">parse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">contents&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">toString&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">global&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get_window_actors&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">window&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">mw&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">meta_window&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">rect&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">mw&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get_frame_rect&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">mw&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get_id&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">w&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">windows&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="kc">null&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">w&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="kc">undefined&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">mw&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">move_to_monitor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">monitor&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">mw&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">move_resize_frame&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">h&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>It loads the saved window position info, then matches the current windows against the IDs of the
saved windows. When there&amp;rsquo;s a match, it restores the window to the correct monitor, then set its
position and size.&lt;/p>
&lt;p>One other thing to note is the &lt;code>sleep(5)&lt;/code> in the Perl code&amp;rsquo;s &lt;code>restore&lt;/code> subroutine. The program needs
to wait for the monitor&amp;rsquo;s input change to take effect, or else none of this works. It&amp;rsquo;d be nice if
&lt;code>display-switch&lt;/code> offered an &lt;code>on_monitor_input_change_execute&lt;/code> config option, but I&amp;rsquo;m not sure if
that&amp;rsquo;s even possible. The &lt;code>sleep&lt;/code> is a hack, but it works fine, so it&amp;rsquo;s good enough for now.&lt;/p>
&lt;p>I just got a docking station for my work laptop, so I&amp;rsquo;ll be able to connect it to both my monitors
as well, and I can use this program on that computer too if I need to.&lt;/p>
&lt;p>I&amp;rsquo;m quite pleased with this solution. I thought it might be anywhere from very hard to impossible,
but this turned out to be fairly easy. Most of my time was spent simply reading about the problem
before discovering the Gnome JS API. Once I knew that API existed, the actual implementation was
fairly easy.&lt;/p>
&lt;p>I also want to credit
&lt;a href="https://www.reddit.com/r/gnome/comments/lwrs1p/moving_and_resizing_windows_programmatically_on/">this /r/gnome post&lt;/a>
by &lt;a href="https://www.reddit.com/user/MortimerErnest/">MortimerErnest&lt;/a>, which links to
&lt;a href="https://gist.github.com/MortenStabenau/130a35a0f2b57b09ca518d202bac0bbe">a bash script they wrote&lt;/a>.
Reading that script made it quite obvious how I could use the Gnome JS API for my own problem.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Well, more than four, because I also have a NAS, a network router, a Nintendo Switch, a PS4 in
the closet, an iPad mini in the same closet, and a Raspberry Pi I bought over a year ago with
which I intended to build an LCD panel clock, though I&amp;rsquo;ve not done so yet. And my phone is also
a computer. This is a very normal number of computers to have.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>The default window manager for GNOME since GNOME 3.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Software Job Search 2022 Retrospective: Coding Challenges</title><link>https://blog.urth.org/2022/04/19/software-job-search-2022-retrospective-coding-challenges/</link><pubDate>Tue, 19 Apr 2022 10:57:22 -0500</pubDate><guid>https://blog.urth.org/2022/04/19/software-job-search-2022-retrospective-coding-challenges/</guid><description>&lt;p>I did a lot of coding and design challenges during my recent job search! A lot a lot. And I have
some thoughts about them.&lt;/p>
&lt;aside>
&lt;small>
&lt;p>Want to read all about my job search? Here&amp;rsquo;s a list of past posts:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://blog.urth.org/2022/03/11/job-search-2022-update-week-1/">Week 1&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.urth.org/2022/03/14/job-search-2022-update-week-1-1/">Week 1.1&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.urth.org/2022/03/19/job-search-2022-update-week-2/">Week 2&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.urth.org/2022/03/25/job-search-2022-update-week-3/">Week 3&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.urth.org/2022/04/02/job-search-2022-update-week-4/">Week 4&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.urth.org/2022/04/10/job-search-2022-update-week-5/">Week 5&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.urth.org/2022/04/15/job-search-2022-update-the-last-one/">The Last One&lt;/a>&lt;/li>
&lt;/ul>
&lt;/small>
&lt;/aside>
&lt;p>I mostly have thoughts about the &lt;em>coding&lt;/em> challenges. The design challenges were pretty much what
you&amp;rsquo;d expect. They started with &amp;ldquo;design a system that has to do X&amp;rdquo;. Once I had an initial design
they&amp;rsquo;d ask some questions about how to handle various types of changes in scale, requirements, etc.&lt;/p>
&lt;p>I&amp;rsquo;ve given design challenges as an interviewer before. I think they&amp;rsquo;re great because it&amp;rsquo;s a chance
to have a conversation on a technical topic with someone that is a &lt;em>lot&lt;/em> like what you&amp;rsquo;d do when
working with them. More places should do these!&lt;/p>
&lt;p>How many of these types of challenges was I given? So many! Here&amp;rsquo;s a list:&lt;/p>
&lt;ul>
&lt;li>Array - 1 take-home coding.&lt;/li>
&lt;li>ClickHouse - 1 take-home coding - but I didn&amp;rsquo;t do this because it came just before I decided to
accept the MongoDB offer.&lt;/li>
&lt;li>Google - 1 live coding - but I didn&amp;rsquo;t continue after this, so there&amp;rsquo;s probably more that they do.&lt;/li>
&lt;li>LogDNA - 1 live system design.&lt;/li>
&lt;li>MongoDB - 3 live coding, 1 live system design.&lt;/li>
&lt;li>Oden - 1 live coding (which I can&amp;rsquo;t remember very well), 1 live system design&lt;/li>
&lt;li>OneSignal - 2 live coding - I withdrew my application before getting a final response from them,
but I think I&amp;rsquo;d already done their full interview process.&lt;/li>
&lt;li>Optic - 1 take-home coding.&lt;/li>
&lt;/ul>
&lt;p>But what about the coding challenges? Let&amp;rsquo;s start at the very beginning, and ask what the purpose of
this type of challenge is. I think there are a few things companies would &lt;em>like&lt;/em> to learn from these
challenges:&lt;/p>
&lt;ul>
&lt;li>Can the candidate understand requirements and build software that fulfills those requirements?&lt;/li>
&lt;li>Can the candidate write code that isn&amp;rsquo;t a complete mess? For example, do they break their code up
into reasonably sized functions/methods/classes/packages?&lt;/li>
&lt;li>Does the candidate understand various technical concepts at the level you&amp;rsquo;d expect given their
experience? This includes topics like concurrency, serialization, REST APIs, etc.&lt;/li>
&lt;/ul>
&lt;h2 id="live-coding">Live Coding&lt;/h2>
&lt;p>&lt;strong>I very strongly question whether you can learn any of these things from a live coding challenge.&lt;/strong>
There are several reasons for this.&lt;/p>
&lt;p>These sorts of exercises are ridiculously artificial, with very little resemblance to real-world
work. They take place in a high-pressure situation in a very limited time. Many of them also further
hamper the candidate with additional constraints.&lt;/p>
&lt;p>It&amp;rsquo;s very common to do these in CoderPad (or an equivalent). &lt;strong>CoderPad is hot steaming garbage and
can go fork itself.&lt;/strong> It&amp;rsquo;s like a half-assed version of an IDE from 20+ years ago. It has very
little customization, no code completion, and is very different from most developers&amp;rsquo; preferred
environments. Even I, a dinosaur who will have Emacs pried out of my cold dead hands, have finally
started used using &lt;a href="https://emacs-lsp.github.io/lsp-mode/">LSP mode&lt;/a> to turn Emacs into a proper
IDE.&lt;/p>
&lt;p>Why do companies use this tool? I don&amp;rsquo;t know. Looking at the feature list, it doesn&amp;rsquo;t seem like it
does anything all that amazing. Pretty much every company was using Zoom for interviews, which let
syou share your screen. CoderPad &lt;em>does&lt;/em> let the interviewer type as well, but there are solutions to
that, like the
&lt;a href="https://code.visualstudio.com/learn/collaboration/live-share">VS Code Live Share extension&lt;/a> or the
Chrome Remote Desktop extension.&lt;/p>
&lt;p>At the very least, I&amp;rsquo;d like to see more companies offer candidates a choice of environments. Of all
the companies I interviewed with, only OneSignal &lt;em>didn&amp;rsquo;t only&lt;/em> use CoderPad (or Google&amp;rsquo;s internal
thing). For one of the challenges we used CoderPad, and then for the other I shared my screen. This
was slightly awkward since I was using Zoom&amp;rsquo;s in-browser version, but its screen sharing is broken
if you try to share your whole screen&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>, but I needed to share multiple apps (Emacs and a
terminal), so I had to quickly install the Zoom Linux app and hope it didn&amp;rsquo;t hard-lock my computer,
as it loves to do.&lt;/p>
&lt;p>The challenges I did live included topics like data (de)serialization, concurrency, and algorithms
and data structures. For the algorithms and data structures ones, I was told not to look things up
online, making the experience even more divorced from normal day-to-day software development. For
the others, I was able to look up things like library APIs, and they tended to be more interactive.&lt;/p>
&lt;p>The worst of these was with Google, where the interviewer was mostly mute as I stumbled through the
problem. The other algorithms interview I had was with MongoDB and the interviewer was more of a
partner in the coding process.&lt;/p>
&lt;p>If I had to evaluate my own performance, I&amp;rsquo;d say that on my two algorithms/data structures
challenges, I did either very poorly (Google) or somewhat poorly (MongoDB). For the others, I did
either very well (finishing the problem in half the allotted time) or fairly well (finishing easily
in the allotted time, but with more looking things up online or getting some help from the
interviewer).&lt;/p>
&lt;p>But when I think about &lt;em>why&lt;/em> I did well on some of these and poorly on others, I think it comes down
almost entirely to prior experience. There were several different data (de)serialization problems
across different companies. I have a &lt;em>lot&lt;/em> of experience with this. I&amp;rsquo;ve helped design
&lt;a href="https://maxmind.github.io/MaxMind-DB/">a somewhat complex binary on-disk data format&lt;/a> for which I
wrote &lt;a href="https://metacpan.org/dist/MaxMind-DB-Writer">the writer&lt;/a>
&lt;a href="https://maxmind.github.io/libmaxminddb/">and&lt;/a>
&lt;a href="https://metacpan.org/dist/MaxMind-DB-Reader">multiple&lt;/a>
&lt;a href="https://metacpan.org/dist/MaxMind-DB-Reader-XS">readers&lt;/a>. I also wrote a
&lt;a href="https://github.com/ActiveState/json-ordered-tidy/">pretty cool (IMHO) JSON tidier&lt;/a>, and I&amp;rsquo;ve dug a
bit into the guts of &lt;a href="https://serde.rs/">serde&lt;/a>,
&lt;a href="https://github.com/mailru/easyjson/pull/339">easyjson&lt;/a>, and lots of Perl code for handling config
files and other formats.&lt;/p>
&lt;p>Unsurprisingly, when presented with a similar problem I can solve it &lt;em>very&lt;/em> quickly. But that&amp;rsquo;s not
because I&amp;rsquo;m an awesome programmer&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>, it&amp;rsquo;s just because I&amp;rsquo;m repeating a task I&amp;rsquo;ve already done many
times.&lt;/p>
&lt;p>Similarly, the concurrency-related tasks weren&amp;rsquo;t too hard, in part because for
&lt;a href="https://github.com/autarch/Crumb/tree/master/web-frontend">my music player frontend&lt;/a> I&amp;rsquo;ve had to
work with async APIs and tasks a lot recently. So solving similar problems feels easy.&lt;/p>
&lt;p>But if my past work history had been different, would I have done nearly as well? Almost certainly
not.&lt;/p>
&lt;p>On the flip side, the two algorithms questions were toy problems that bore no resemblance to any
work I&amp;rsquo;ve ever done&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>. If I had to do something similar for work, I&amp;rsquo;d google the answer, cut,
paste, and tweak some code, and probably have something reasonable working soon enough. But without
that &amp;ldquo;crutch&amp;rdquo;&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup> to lean on, I didn&amp;rsquo;t do very well.&lt;/p>
&lt;p>Given all that, I&amp;rsquo;m very skeptical that the interviewers got good answers to the questions I think
they should be asking. Instead, I think they mostly learned if I&amp;rsquo;d done a similar thing in the past
or not. That is somewhat useful information. I guess. Maybe. But I think it&amp;rsquo;s a pretty poor
indicator of my future job performance.&lt;/p>
&lt;h2 id="take-homes">Take-Homes&lt;/h2>
&lt;p>It&amp;rsquo;s obvious to me that take-homes do a much better job of answering the questions I posed above.
The candidate can do them in a reasonable time frame, with less pressure, and in a familiar coding
environment.&lt;/p>
&lt;p>However, take-homes have a few big disadvantages for both the candidate and employer.&lt;/p>
&lt;p>The big one is that they take longer in several ways. They use more of the candidate&amp;rsquo;s time, which
is annoying for the candidate. For the take-homes I was given, I was told they should take 2-4
hours, as opposed to all of the live coding exercises, which were scheduled for 45 minutes or an
hour.&lt;/p>
&lt;p>And because it&amp;rsquo;s more time-consuming for candidates, it means that they may just not bother. We saw
non-trivial attrition during this step of our hiring process at both MaxMind and ActiveState. We&amp;rsquo;d
give them the take-home challenge and they&amp;rsquo;d disappear. I certainly don&amp;rsquo;t blame them!&lt;/p>
&lt;p>It also tends to slow down the hiring process. The employer has to give the candidate a reasonable
amount of time to do this. Most places gave me 3-7 days as a default, with a provision saying &amp;ldquo;let
us know if you need more time&amp;rdquo;. That&amp;rsquo;s a 3- to 7-day stall in the hiring process. During that time
the candidate might finish a bunch of live coding interviews with other companies and get an offer!&lt;/p>
&lt;p>The other issue I saw with take-homes is that they&amp;rsquo;re harder to scope well. Notably, I think Array&amp;rsquo;s
exercise was a bit under-specified and could have used more clarification of what was in scope and
out of scope. I think I spent more time on it than was needed because of this.&lt;/p>
&lt;h3 id="optics-challenge-and-why-it-was-the-best">Optic&amp;rsquo;s Challenge and Why It Was the Best&lt;/h3>
&lt;p>As I mentioned in a previous post, I really liked how Optic structured their challenge. They&amp;rsquo;re a
remote company trying to work largely asynchronously, and the challenge reflects this. It started
with an invite to a fresh Git repo that included the instructions as well as some existing code. The
work involved extending an existing service in the repo with some functionality, though you could do
this by writing a new service for just the new bits of the API. They also invited me to a Slack
channel just for this challenge.&lt;/p>
&lt;p>They encouraged me to treat this like I would if I was actually working there. So rather than just
taking the instructions and working in isolation, I started by asking for a quick Zoom call with one
of their devs&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup> to clarify a few points. Then later in the process, I filed some GitHub issues to
ask more questions about details of the project and scope. The same dev responded on GitHub and we
discussed the pros and cons of each implementation.&lt;/p>
&lt;p>Another thing I liked was that they not only asked for code, they also asked for some documentation
around design choices, trade-offs I&amp;rsquo;d made, and future directions for improvements. This is exactly
the type of thing you&amp;rsquo;d do at work, right? The main difference is that you&amp;rsquo;d probably do some of
that documentation &lt;em>first&lt;/em> as part of the feature scoping. Then the final deliverable could include
some documentation/stories/tasks/tickets/whatever for next steps on the project.&lt;/p>
&lt;p>And finally, they paid me $300 for doing this. FWIW, I understand why this can be a bit tricky at
some places, so I think a good alternative would be to offer to make a donation to a 501(c)(3)&lt;sup id="fnref:6">&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref">6&lt;/a>&lt;/sup>
charity on the candidate&amp;rsquo;s behalf.&lt;/p>
&lt;h2 id="what-about-existing-projects">What About Existing Projects?&lt;/h2>
&lt;p>&lt;em>None&lt;/em> of the companies that gave me any of these challenges offered to let me submit an existing
project as a replacement. This surprised me. I didn&amp;rsquo;t ask any places about this. In retrospect, I
wish I had, just out of curiosity as to why.&lt;/p>
&lt;p>My &lt;em>guess&lt;/em> is that they would say that they want to give all candidates the same process in the
interest of equity. But I&amp;rsquo;m &lt;em>extremely&lt;/em> skeptical that in practice this improves diversity in
hiring.&lt;/p>
&lt;p>If you&amp;rsquo;re coming into tech from an underrepresented background, are you more or less likely to have
the free time and energy to spend three hours per company on take-homes? Are you more or less likely
to get nervous and freeze up during a live coding exercise?&lt;/p>
&lt;p>If anyone has more data about this &lt;a href="mailto:autarch@urth.org">I&amp;rsquo;d be very curious to learn more&lt;/a>.&lt;/p>
&lt;h2 id="takeaways">Takeaways&lt;/h2>
&lt;p>Ironically, when I was given the choice by OneSignal&lt;sup id="fnref:7">&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref">7&lt;/a>&lt;/sup>, I chose live coding over a take-home. I
was gambling that it would go well and I wanted to save some time. For me, live coding was better
because it&amp;rsquo;s quick and I was fairly confident I could quickly handle most types of problems that I
would get in these sessions.&lt;/p>
&lt;p>I think more companies should offer this choice. This lets the candidate pick the option that they
think will best showcase their skills.&lt;/p>
&lt;p>But I think it&amp;rsquo;s probably &lt;em>worse&lt;/em> for the company doing the hiring.&lt;/p>
&lt;p>Is there any better way to evaluate someone&amp;rsquo;s abilities besides a take-home or looking at existing
public projects? I can&amp;rsquo;t think of one. In my time in software engineering, I&amp;rsquo;ve seen a lot of people
hired as developers who were fundamentally incapable of good work for a variety of reasons. Getting
answers to the questions I posed above is critical for hiring.&lt;/p>
&lt;p>So we&amp;rsquo;re left in this state where it&amp;rsquo;s hard to hire software engineers, but we feel like we have to
put them through the wringer in the interview process anyway. What a silly, silly field I&amp;rsquo;m in!&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>I&amp;rsquo;m going to blame this on Wayland.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Though I am ;)&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>I&amp;rsquo;d honestly be surprised if &lt;em>any&lt;/em> software developer I met had done something similar outside
of leetcode-type exercises, though I&amp;rsquo;m sure someone somewhere has.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>Of course, this isn&amp;rsquo;t a crutch. No sane employer will ever complain that you found an answer
quickly online as opposed to spending longer solving it from first principles in isolation!&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>Yes, I know I said they work asynchronously, but they also made it clear that a kickoff Zoom
call was an option.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:6">
&lt;p>Or your country&amp;rsquo;s equivalent.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:7">
&lt;p>The only company that offered this choice, IIRC.&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Job Search 2022 Update: The Last One</title><link>https://blog.urth.org/2022/04/15/job-search-2022-update-the-last-one/</link><pubDate>Fri, 15 Apr 2022 16:39:33 -0500</pubDate><guid>https://blog.urth.org/2022/04/15/job-search-2022-update-the-last-one/</guid><description>&lt;p>Assuming nothing unexpected and unhappy happens, this will be my final Job Search 2022 update. I
accepted the offer from MongoDB and I start in a few weeks! Thanks again to David Golden for
reaching out to me about MongoDB way back in early March.&lt;/p>
&lt;p>Ultimately, my thinking came down to a decision between startups and public companies. I realized
that given my age and my early retirement goal, I can best achieve that goal by focusing on higher
total compensation that&amp;rsquo;s more secure&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>, rather than going for another startup lottery ticket.&lt;/p>
&lt;p>&lt;strong>I want to give a special shout out to Optic.&lt;/strong> Of all the places I interviewed with, I liked their
hiring process the most&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>. The way they structure their take home coding assignment is a bit
different from others, and I&amp;rsquo;ll go into that in a later post. They are also paying me $300 for doing
it, and are the only company that has done this during my search. And as I noted in my last update,
their offer includes options expressed as a percentage of the company, which is another outlier. All
of this makes me think that working there would be great.&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://useoptic.notion.site/Optic-is-hiring-9af73ddc8fd44776a6b4d7339aff6c68">They are still hiring&lt;/a>
and if you want to get in on an early stage startup that&amp;rsquo;s not looking to eat your entire life I
highly recommend applying.&lt;/strong>&lt;/p>
&lt;p>So that&amp;rsquo;s this week&amp;rsquo;s TLDR, but I&amp;rsquo;ll still do the regular full update of all happenings in the past
week.&lt;/p>
&lt;p>The list of companies that never responded is still the same:&lt;/p>
&lt;ul>
&lt;li>GitHub - applied on 2022-03-10&lt;/li>
&lt;li>Netflix - applied on 2022-03-11&lt;/li>
&lt;li>Oso - applied on 2022-03-10&lt;/li>
&lt;/ul>
&lt;p>I honestly am questioning my sanity a little here. I do have an automated email from Oso thanking me
for my application, but I don&amp;rsquo;t see one from GitHub or Netflix. Did I not apply somehow? Do they
have a system that doesn&amp;rsquo;t send automated &amp;ldquo;thanks for applying&amp;rdquo; emails? I checked my spam folder.
Nothing. Maybe I&amp;rsquo;ll never know. Maybe I&amp;rsquo;ll get a response three months from now and have a laugh.&lt;/p>
&lt;p>So what else happened this past week?&lt;/p>
&lt;p>Things had gone well with Array, but on reflection I realized I didn&amp;rsquo;t understand their business
well enough. Specifically, I have ethical concerns about fintech in general and their credit tools
specifically. I don&amp;rsquo;t think this sort of thing is &lt;em>automatically&lt;/em> bad, but I can imagine it being
used for evil. I realized there&amp;rsquo;s no way I can figure this out short of working there for a few
years. So I withdrew my application.&lt;/p>
&lt;p>ClickHouse asked me to do a take home assignment this week. But I got it when I was pretty sure I&amp;rsquo;d
be accepting a different offer on Friday, so I didn&amp;rsquo;t start it. I withdrew my application today.&lt;/p>
&lt;p>I had two interviews scheduled for Monday afternoon with Fastly, but when I went to join the first
one I realized the interviewer had not accepted the calendar invite. I think it&amp;rsquo;s because he&amp;rsquo;s
someone I&amp;rsquo;ve hung out a lot with at Perl conferences in the past and so it wasn&amp;rsquo;t appropriate for
him to interview me. But the person doing scheduling hadn&amp;rsquo;t noticed this. So I had one of two
interviews that day, and the other was rescheduled with someone else for Wednesday.&lt;/p>
&lt;p>I emailed the recruiter saying I wanted to move things along quickly, and ultimately the final
interviews were scheduled for Friday the 15th. But at the same time, I&amp;rsquo;d started negotiating on
final offer details with MongoDB. MongoDB&amp;rsquo;s recruiter called me back Friday morning and they met my
increased ask, so I accepted&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>So I cancelled the Fastly interviews, which they had scheduled for me pretty quickly to meet my
timeline. I do feel bad about that, but I would note that my application was submitted as an
internal referral on March 9, 37 days ago&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>. I didn&amp;rsquo;t have my first interview with the relevant
hiring manager until April 6, just nine days ago.&lt;/p>
&lt;p>A couple folks I know through Perl who now work at Google reached out on Monday. One works in
developer relations and though that this would be a good fit for me. I asked whether it was possible
to move super fast through the hiring process, because I wasn&amp;rsquo;t going to ask MongoDB and others to
wait for several more weeks on the &lt;em>hope&lt;/em> of an offer from Google. They thought it might be.&lt;/p>
&lt;p>But it wasn&amp;rsquo;t, so this ended up not going anywhere. I wish I&amp;rsquo;d reached out to them directly earlier.
From talking to them, it sounds like I&amp;rsquo;d have a better chance of getting hired for a developer
relations position than the software engineering position a recruiter submitted me for at the
beginning of March. It would&amp;rsquo;ve been kind of poetic if the very first place I interviewed, where I
had a terrible experience, turned out to be the &lt;em>last&lt;/em> place I interviewed and I ended up working
there.&lt;/p>
&lt;p>During my very first call with the recruiter from LogDNA, I told him (and later the hiring manager)
about my plans to spend six months in Taiwan in 2023-2024. I tried to make sure I brought this up
early with every company in case it was a blocker. So &amp;hellip; it turned out to be a blocker. Needless to
say, this was a bit frustrating since I&amp;rsquo;d already spent several hours in interviews with them and
they&amp;rsquo;d made an offer. On the plus side, at least I didn&amp;rsquo;t &lt;em>also&lt;/em> spend several hours on a take home
assignment too.&lt;/p>
&lt;p>Oden decided not to move forward. I didn&amp;rsquo;t get any feedback on why.&lt;/p>
&lt;p>I hadn&amp;rsquo;t heard anything from OneSignal this week, so there are no updates about them other than that
I withdrew my application.&lt;/p>
&lt;h2 id="parting-thoughts">Parting Thoughts&lt;/h2>
&lt;p>One of the questions I like to ask companies, especially when talking to people at the
director/VP/C-suite level, is what challenges the company is facing now and expects to face in the
future. A number of places said that hiring was a challenge. This is no surprise in such a hot
market. But then why are some companies &lt;em>still so slow in their hiring processes&lt;/em>?&lt;/p>
&lt;p>That&amp;rsquo;s my biggest complaint about my job search. Many companies are surprisingly slow, either to
just &lt;em>respond&lt;/em> to an application, or to move the process along as it goes. To be fair, I complained
about this on the hiring side at past employers as well.&lt;/p>
&lt;p>I think more companies need to figure out how to make hiring their top priority for the people
involved. There&amp;rsquo;s no reason not to try to schedule all the interviews in the space of a week. If
there&amp;rsquo;s a take home assigment involved, you should give candidates a reasonable amount of time (one
week is a good baseline), but then schedule everything else quickly once that&amp;rsquo;s done. This is
especially true with a candidate like me who is jobless. I had plenty of free time to interview, and
would&amp;rsquo;ve loved to go faster everywhere.&lt;/p>
&lt;p>Otherwise I don&amp;rsquo;t have &lt;em>too&lt;/em> many complaints about the various processes I went through, except for
a few live coding challenges. I&amp;rsquo;ll get into that in a future retrospective post.&lt;/p>
&lt;p>I&amp;rsquo;m quite happy to be done with the job search. It was exhausting! And I&amp;rsquo;m excited to start at
MongoDB soon. I think the work will be interesting, and everything I know about the company says it
will be a great place to work.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>&lt;a href="https://www.investopedia.com/terms/r/restricted-stock-unit.asp">RSUs&lt;/a> aren&amp;rsquo;t &lt;em>guaranteed&lt;/em> to
have any value in the future, but they&amp;rsquo;re a more reliable value than equity in a private
company.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Though all of the companies that I got into the process with were pretty good, modulo some being
too slow.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>I &lt;em>hate&lt;/em> negotiating. It always leaves me wondering if I should&amp;rsquo;ve asked for me. This is why I
prefer public well-defined salary levels.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>&lt;code>perl -MDateTime -E 'my $dur = DateTime-&amp;gt;today-&amp;gt;delta_days(DateTime-&amp;gt;new(year =&amp;gt; 2022, month =&amp;gt; 3, day =&amp;gt; 9)); say $dur-&amp;gt;in_units(q{days})'&lt;/code>&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Job Search 2022 Update: Week 5</title><link>https://blog.urth.org/2022/04/10/job-search-2022-update-week-5/</link><pubDate>Sun, 10 Apr 2022 19:16:28 -0500</pubDate><guid>https://blog.urth.org/2022/04/10/job-search-2022-update-week-5/</guid><description>&lt;p>It&amp;rsquo;s week 5 and my brain is tired. Fortunately, I think I&amp;rsquo;m near the end. My current plan is to
decide by Friday, April 15. I suspect that some of the companies I&amp;rsquo;m talking to won&amp;rsquo;t be in a
position to make an offer then, but I already have some good offers, I want to finish this process,
and there&amp;rsquo;s a limit to how long I can ask people to wait for me to decide.&lt;/p>
&lt;p>Once again, I&amp;rsquo;ll start with the the list of places that are just sitting on my application:&lt;/p>
&lt;ul>
&lt;li>GitHub - applied on 2022-03-10&lt;/li>
&lt;li>Netflix - applied on 2022-03-11&lt;/li>
&lt;li>Oso - applied on 2022-03-10&lt;/li>
&lt;/ul>
&lt;p>So it&amp;rsquo;s basically been a month for all of these now.&lt;/p>
&lt;p>This past week also had quite a few interviews, as well as some interesting developments that
started on Saturday, April 2. That day, someone shared &lt;a href="https://blog.urth.org/2022/03/28/yet-another-github-profile-generator/">my blog post about my GitHub profile
generator&lt;/a> on
&lt;a href="https://news.ycombinator.com/item?id=30886620">Hacker News&lt;/a>&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>A hiring manager at Meta (aka Facebook) saw this post, realized I was in the midst of my job search,
and reached out to me to see if I was interested in talking to them. I had kind of ruled Meta out
because I&amp;rsquo;m unsure whether Facebook is a net negative for the world&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>, as opposed to being
positive or neutral. But interest is always flattering, and they pay the megabucks, so I figured it
couldn&amp;rsquo;t hurt to learn more. I spoke to this manager on Monday, which was helpful.&lt;/p>
&lt;p>They ended up connecting me to someone in Production Engineering for another informational
interview. PE is Facebook&amp;rsquo;s SRE, but a little different. Ultimately, I ended up deciding not to
apply for any position there, for a few reasons.&lt;/p>
&lt;p>One reason is that I&amp;rsquo;d almost certainly have had to put in some study time for either a software dev
or PE job, and I don&amp;rsquo;t have time to do that on my current schedule. Second, at least the PE jobs
involve a lot of C++, which I&amp;rsquo;d like to avoid.&lt;/p>
&lt;p>But the third and most important reason is that after thinking about this a lot I&amp;rsquo;m &lt;em>still&lt;/em> unsure
about whether Facebook is a net negative for the world. I just can&amp;rsquo;t see myself working somewhere
that I&amp;rsquo;m not sure is at least neutral.&lt;/p>
&lt;p>On to the rest of the updates &amp;hellip;&lt;/p>
&lt;p>I had a few more interviews with folks at Array. I think these went fairly well.&lt;/p>
&lt;p>I met with the recruiter at ClickHouse and then had an interview with the hiring manager of the
position I applied for. I think it went well, although I&amp;rsquo;m not sure if the position is exactly what
I thought it was. I&amp;rsquo;ll have to learn more about it if I move forward.&lt;/p>
&lt;p>Cockroach Labs is no longer on the &amp;ldquo;hasn&amp;rsquo;t responded&amp;rdquo; list. They responded with a rejection. It took
them three weeks, but that&amp;rsquo;s better than not responding at all. Yes, I&amp;rsquo;m looking at you, GitHub,
Netflix, and Oso.&lt;/p>
&lt;p>I had my first interview with a hiring manager at Fastly. This went well, and I have several more
interviews scheduled for Monday, 2022-04-11. I did emphasize that I&amp;rsquo;d like to move fast, and I
appreciate their responsiveness to that.&lt;/p>
&lt;p>MongoDB made an offer! It&amp;rsquo;s a good offer. But like with LogDNA, I told them that I wanted to decide
this coming week. I also have a call scheduled with the VP of the division (team? department?
sector?) I&amp;rsquo;d be on at MongoDB. I greatly appreciate the chance to talk to VPs and CEOs post-offer.
Every company should do this.&lt;/p>
&lt;p>I haven&amp;rsquo;t heard back from Oden. I will poke them this week if I don&amp;rsquo;t hear something soon.&lt;/p>
&lt;p>I had my virtual onsite with OneSignal. This was a mix of interviews and coding challenges. For the
coding challenge, I was warned that maybe I shouldn&amp;rsquo;t do it in Rust, so I did it in Rust anyway. It
involved concurrency, which I&amp;rsquo;ve had to deal with in one of my personal Rust projects recently, so
this worked out okay. I ended up learning about some new aspects of &lt;a href="https://tokio.rs/">&lt;code>tokio&lt;/code>&lt;/a>,
which was fun. I probably could&amp;rsquo;ve done it in Go or Perl too, but I haven&amp;rsquo;t done anything concurrent
in either recently, and I didn&amp;rsquo;t want to spend the entire time reading docs.&lt;/p>
&lt;p>I also had a technical challenge with Optic. They structure this in a really interesting way to make
it more of a work simulation. This challenge went well, and they made an offer! It&amp;rsquo;s a good offer.
Of course, I told them that I wanted to decide this coming week.&lt;/p>
&lt;p>I have to give a huge shout-out to Aidan, Optic&amp;rsquo;s CEO, for one small but very important detail in
the offer. &lt;strong>The equity portion of the offer was described as a percentage of the company.&lt;/strong> This is
the only number for equity that matters in most cases&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>. Many companies just give you a number of
shares and the strike price. But without knowing what percentage of the company those options
represent, this is not very meaningful.&lt;/p>
&lt;p>So things have been going well, and I&amp;rsquo;m very optimistic about finishing up this coming week. Stay
tuned for what might be my last update in a week or so. I also plan to write a few retrospective
posts on recruiters, coding challenges, and the ethical impact of my work after that.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Why didn&amp;rsquo;t I share this myself? I missed out on 83 points of karma! 83!&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>I feel the same way about much of social media.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>Strike price can matter too, but only with a smaller exit (probably an acquisition) where the
spread between strike price and share purchase price isn&amp;rsquo;t that big. But if the stock is trading
at $250/share, the difference between a $2 and $20 strike price is minimal.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Job Search 2022 Update: Week 4</title><link>https://blog.urth.org/2022/04/02/job-search-2022-update-week-4/</link><pubDate>Sat, 02 Apr 2022 10:07:37 -0500</pubDate><guid>https://blog.urth.org/2022/04/02/job-search-2022-update-week-4/</guid><description>&lt;p>Has it been four weeks? I can&amp;rsquo;t remember. I&amp;rsquo;ve been told there was a time before the interviews,
before the coding challenges, before the system design questions. But I can&amp;rsquo;t remember. That life is
gone, lost in the haze. All I have now is the interviews, the endless stream of questions and
scheduling requests. My only hope now is that this infinity will end, but how can infinity end?
That&amp;rsquo;s the paradox that consumes my waking thoughts.&lt;/p>
&lt;p>If I add it all up, I had 11.5 hours of interviews this past week. That doesn&amp;rsquo;t seem like much when
I write it down, but it sure &lt;em>felt&lt;/em> like a lot while I was doing it.&lt;/p>
&lt;p>Fun fact, a lot of the people I talk to have read these blog posts! That&amp;rsquo;s not surprising, since I
link to this blog from my resume and online profiles. From what I can tell, nothing I&amp;rsquo;ve written has
alarmed anyone &lt;em>too&lt;/em> much.&lt;/p>
&lt;p>Originally, in my &lt;a href="https://blog.urth.org/2022/03/19/job-search-2022-update-week-2/">week 2 post&lt;/a> I had said I am &amp;ldquo;terminally blunt&amp;rdquo; and someone
did ask about that. I realized that wasn&amp;rsquo;t the best wording, and I&amp;rsquo;ve since edited that blog post.
What I was trying to express was that I don&amp;rsquo;t like the types of social lies where I&amp;rsquo;m lying and the
person I&amp;rsquo;m speaking to knows I&amp;rsquo;m lying, but social convention says I should lie anyway. I&amp;rsquo;d rather
just tell the truth. But the phrase &amp;ldquo;terminally blunt&amp;rdquo; makes me sound like I could be a huge
asshole. I will leave it to those who know me to judge whether I am, but I am sure that none of my
references will say that I am.&lt;/p>
&lt;p>Again, I&amp;rsquo;ll start with the list of places that simply have not responded beyond the initial
automated &amp;ldquo;thanks for your application&amp;rdquo;:&lt;/p>
&lt;ul>
&lt;li>Cockroach Labs - applied on 2022-03-21&lt;/li>
&lt;li>GitHub - applied on 2022-03-10&lt;/li>
&lt;li>Netflix - applied on 2022-03-11&lt;/li>
&lt;li>Oso - applied on 2022-03-10&lt;/li>
&lt;/ul>
&lt;p>I added Cockroach Labs since it&amp;rsquo;s been two weeks since my application now.&lt;/p>
&lt;p>ClickHouse is no longer on the list! I have a first interview with someone there next Monday. Can I
get through the process with them and other slow movers before I have enough offers that I should
just pick one? Maybe they&amp;rsquo;ll move fast. Let&amp;rsquo;s see.&lt;/p>
&lt;p>I didn&amp;rsquo;t submit any &lt;em>new&lt;/em> applications this week, and at this point I&amp;rsquo;m responding to all&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> new
incoming requests with &amp;ldquo;thanks but I&amp;rsquo;m too busy for more interviews&amp;rdquo;.&lt;/p>
&lt;p>Here&amp;rsquo;s this week&amp;rsquo;s updates &amp;hellip;&lt;/p>
&lt;p>I had an initial call with a recruiter at 1Password. I had applied for an engineering manager
position because I thought from the job description the role involved a fair bit of IC work too. But
it was much less than I wanted. The recruiter said they&amp;rsquo;d run my resume and interests by all hiring
managers to see if maybe there was a management position that fit what I wanted, or if they wanted
to consider me for an IC role. The next day I got a generic &amp;ldquo;we&amp;rsquo;ve decided not to move forward with
the role you applied for&amp;rdquo; email. So I&amp;rsquo;m not sure if they actually considered me for anything else.&lt;/p>
&lt;p>Maybe they thought I was trying to do some sort of bait and switch application. Is that a thing? How
could that possibly work?. I didn&amp;rsquo;t apply for any IC roles because at the time I didn&amp;rsquo;t see any that
seemed like a good fit. Maybe I was right and that hasn&amp;rsquo;t changed. Oh well.&lt;/p>
&lt;p>I still hadn&amp;rsquo;t heard back from Array by Tuesday, March 29, six days after submitting my homework, so
I emailed the recruiter to check in. My homework was well received and they just needed to get the
CTO&amp;rsquo;s feedback. I ended up meeting with the CTO on Friday morning and he was very positive about my
application. They&amp;rsquo;re hiring for several teams so we talked about which might be the best fit for me,
and I&amp;rsquo;ll move forward with one of those teams next week.&lt;/p>
&lt;p>I spoke to a different recruiter at Fastly and I have an interview with a hiring manager scheduled
for next Wednesday. I&amp;rsquo;d like to move this one along since at one interview per week the process will
take way too long. If things go well next week I will politely tell the hiring manager that.&lt;/p>
&lt;p>I got an offer from LogDNA! It&amp;rsquo;s a good offer, but I told them that I need to wait to see other
offers before I make any decisions.&lt;/p>
&lt;p>I had a metric forkton of interviews with MongoDB on Monday and Tuesday. This included a couple live
coding exercises, one on concurrency and one on algorithms, as well as a live systems design
challenge. I think I did really well on the concurrency exercise, fairly well on the systems design
challenge, and not so well on the algorithms exercise. I have a lot more to say about these sorts of
live exercises, as well as homework/take-homes, but I&amp;rsquo;ll save it for after I accept an offer&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>I did a live coding exercise with OneSignal as well. This went quite well. I have a &amp;ldquo;virtual onsite&amp;rdquo;
(aka circa three hours of interviews) scheduled for next week.&lt;/p>
&lt;p>One thing about OneSignal that stands out is the detailed interview guide materials that they give
you. These materials detail what types of interviews you&amp;rsquo;ll be having, what topics they&amp;rsquo;ll cover,
the qualities they&amp;rsquo;re looking for, and they give some guidance on how to prepare. This is great!
Every company should do this! If I end up somewhere beside OneSignal I might try to make this happen
there.&lt;/p>
&lt;p>I met with several people from Oden, including the CEO, another engineering manager, a software
engineer, and a product person. This included a systems design challenge with an interesting
structure. For the first hour, I worked with an engineer who presented the problem. I sketched out a
design in Lucidchart and made a bunch of notes. Then after a short break, I met with a product
person. I explained the design to them and we talked about various potential issues.&lt;/p>
&lt;p>If I had to pick one company right now based purely on the &lt;em>product&lt;/em> (ignoring offer details, tech
stack, company size, etc.), it&amp;rsquo;d probably be Oden. As a refresher, they do data collection and
analytics for factory production lines. This is quite interesting, and it&amp;rsquo;d be totally new for me.&lt;/p>
&lt;p>But of course, all those other details besides the product matter a lot too. And fortunately, every
company I&amp;rsquo;ve spoken to has an interesting product that will involve some sort of new challenge for
me, whether that be its scale, learning a new field, or something else.&lt;/p>
&lt;p>I had a couple more interviews with Optic, including one with an engineer and another chat with
their CEO. They&amp;rsquo;re a very early stage startup (just 5 people right now, I believe), but it sounds
like they have a reasonable work/life balance. I greatly enjoy having a chance to talk to company
CEOs and learn more about the product vision, their growth and funding plans, the challenges they
face beyond the technical ones, and other high-level details. This is a perk of interviewing with
smaller companies, and it was an aspect of my interview process at ActiveState I liked a lot back
in 2017.&lt;/p>
&lt;p>And that&amp;rsquo;s this week&amp;rsquo;s update. The list is slowly shrinking, and I&amp;rsquo;m hoping that I will be able to
make a decision by Friday, April 15 at the latest. After that, I suspect the gating factor on my
start date will be laptop availability.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>If someone reaches out about a job that pays $1 million a year with 20 weeks of PTO and a 4-day
week, I&amp;rsquo;ll definitely respond. Again, my email is &lt;a href="mailto:autarch@urth.org">autarch@urth.org&lt;/a>.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Translation, I&amp;rsquo;m gonna complain about these big time but I don&amp;rsquo;t want to do it mid-process in
case it makes any potential employer mad.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Yet Another GitHub Profile Generator</title><link>https://blog.urth.org/2022/03/28/yet-another-github-profile-generator/</link><pubDate>Mon, 28 Mar 2022 10:09:46 -0500</pubDate><guid>https://blog.urth.org/2022/03/28/yet-another-github-profile-generator/</guid><description>&lt;p>A number of the companies I&amp;rsquo;ve been interviewing with are using &lt;a href="https://guide.co/">Guide&lt;/a> to create
an &amp;ldquo;interview guide&amp;rdquo; that they send to candidates. I really like this. It includes information about
the interview topic, interviewer profiles, and relevant links to pages on the company website and/or
blog.&lt;/p>
&lt;p>For one company, an interviewer profile linked to
&lt;a href="https://github.com/esatterwhite">that person&amp;rsquo;s GitHub profile&lt;/a>, so I took a look. Instead of the
standard profile, they have a custom thing with lots of fun stats about their activity on GitHub.
Neat!&lt;/p>
&lt;p>But how do I do that? It took me an embarrassingly long time to figure out what the GitHub feature
for this was named. The docs call it
&lt;a href="https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/customizing-your-profile/managing-your-profile-readme">&amp;ldquo;Managing your profile README&amp;rdquo;&lt;/a>.
Doing it is simple. If your account name is &amp;ldquo;githubuser&amp;rdquo;, you make a new repo called &amp;ldquo;githubuser&amp;rdquo;.
Then any &lt;code>README.md&lt;/code> in that repo will be displayed on your GitHub profile page.&lt;/p>
&lt;p>I think this feature has existed for a long time but I somehow managed to miss it. I took a look at
how &lt;a href="https://github.com/esatterwhite">the profile I linked to, Eric&amp;rsquo;s&lt;/a>, is generated. It&amp;rsquo;s using the
&lt;a href="https://github.com/lowlighter/metrics/">&lt;code>lowlighter/metrics&lt;/code> GitHub Actions&lt;/a>. Each plugin generates
an infographic, and you control what is generated by which plugins you use in your GitHub Actions
workflow. The actual generation uses GitHub&amp;rsquo;s API (or other services for a few plugins).&lt;/p>
&lt;p>But while I like how it looks, I &lt;em>don&amp;rsquo;t&lt;/em> like that it only generates images. This means that nothing
is clickable, so you can&amp;rsquo;t link to recent PRs or repos with recent activity, for example.&lt;/p>
&lt;p>Fortunately, there are &lt;em>many&lt;/em> other options. A search for &amp;ldquo;github profile generator&amp;rdquo; finds many
options. One of the most popular appears to be
&lt;a href="https://rahuldkjain.github.io/gh-profile-readme-generator/">Rahul Jain&amp;rsquo;s&lt;/a>. You fill out a web form
and it gives you some markdown to turn into your profile.&lt;/p>
&lt;p>But I don&amp;rsquo;t like this one either. The content it generates is essentially static. If you want to
update it you need to go to the form and change things (though you can save your config between
uses).&lt;/p>
&lt;p>I wanted something that would regenerate regularly, and I wanted it to include whatever I wanted. I
also wanted the output to be Markdown (without lots of images) so it could have clickable links.
There was nothing else to do but to build yet another profile generator!&lt;/p>
&lt;p>Does the world need another profile generator? No, it doesn&amp;rsquo;t. Did I do it anyway? Yes, I did.&lt;/p>
&lt;p>You can &lt;a href="https://github.com/autarch">see its output in my profile right now&lt;/a>.&lt;/p>
&lt;h2 id="how-it-works">How It Works&lt;/h2>
&lt;p>I wrote it in Rust. Is Rust the best language for this? Yes, because I want to learn more Rust.&lt;/p>
&lt;p>&lt;a href="https://github.com/autarch/autarch">The code lives in the &lt;code>autarch/autarch&lt;/code> repo itself&lt;/a>.&lt;/p>
&lt;p>I started with &lt;a href="https://docs.rs/octorust/latest/octorust/">&lt;code>octorust&lt;/code>&lt;/a>, which uses
&lt;a href="https://docs.github.com/en/rest">GitHub&amp;rsquo;s REST API (the V3 API)&lt;/a>. REST APIs are usually quite easy
to understand, even though you end up with a lot of API calls in many cases. However, at one point I
found a weird bug in &lt;code>octorust&lt;/code> where the
&lt;a href="https://docs.rs/octorust/latest/octorust/repos/struct.Repos.html#method.list_languages">&lt;code>Repos::list_languages&lt;/code> method&lt;/a>
inexplicably returns a &lt;code>Result&amp;lt;i64&amp;gt;&lt;/code> instead of
&lt;a href="https://docs.github.com/en/rest/reference/repos#list-repository-languages">the data it &lt;em>should&lt;/em> return&lt;/a>.&lt;/p>
&lt;p>The &lt;code>octorust&lt;/code> crate is entirely generated, and the generated code lives alongside the generator in
&lt;a href="https://github.com/oxidecomputer/third-party-api-clients">the &lt;code>oxidecomputer/third-party-api-clients&lt;/code> repo&lt;/a>.
That repo has issues disabled, so I couldn&amp;rsquo;t report this (though it does allow PRs).&lt;/p>
&lt;p>In the meantime, I&amp;rsquo;d been looking at how &lt;code>lowlighter/metrics&lt;/code> was implemented and saw that it was
using &lt;a href="https://docs.github.com/en/graphql">GitHub&amp;rsquo;s GraphQL API (the V4 API)&lt;/a>. I find GraphQL more
challenging to use than REST since you need to formulate queries from scratch, rather than using
predefined REST endpoints that correspond to specific resources. But the big advantage of GraphQL is
that you can get exactly the data you want across multiple resources in a single query.&lt;/p>
&lt;p>I decided that this was a good opportunity to learn something new, so I took some queries from the
&lt;code>lowlighter/metrics&lt;/code> repo and started massaging them to give me what I wanted.&lt;/p>
&lt;p>At first, I tried a single query to give me all the repos to which I had access via the
&lt;a href="https://docs.github.com/en/graphql/reference/objects#user">User repositories field&lt;/a>. But for some
reason, this seemed to skip some repos in
&lt;a href="https://github.com/houseabsolute/">my houseabsolute organization&lt;/a>. There&amp;rsquo;s a good chance this was
user error on my part, but I eventually ended up simply doing one query for the user and one for the
org.&lt;/p>
&lt;p>I used &lt;a href="https://lib.rs/crates/graphql_client">the &lt;code>graphql_client&lt;/code> crate&lt;/a> to make these requests. At
first, I used its macros to generate code at compile time from my queries. But the huge downside of
this is that it doesn&amp;rsquo;t play very nicely with code completion. The generated structs are quite
complex, and the only way for me to find out what fields they contained was to use
&lt;a href="https://github.com/dtolnay/cargo-expand">&lt;code>cargo-expand&lt;/code>&lt;/a>. But that didn&amp;rsquo;t help with code
completion.&lt;/p>
&lt;p>Fortunately, the crate also includes a &lt;code>graphql-client&lt;/code> binary that will generate the same code that
the macro does. This was much better. I could open the code in Emacs to view the generated structs
and modules, and code completion just worked.&lt;/p>
&lt;p>There was still one unfortunate issue, which is that even though my queries for user repos and org
repos are nearly identical, the generator produces different Rust types for the user&amp;rsquo;s list of repos
versus the org&amp;rsquo;s. This means that my stats collection code needs to be repeated for each type.&lt;/p>
&lt;p>There &lt;em>are&lt;/em> a couple of ways I could fix it. One would be to create a trait that each type can
implement. But traits are defined in terms of methods. That means I&amp;rsquo;d have to write methods for each
field in common between the types that I wanted to access. I did try this, but then I ran into
lifetime issues when trying to take references to data in these structs. I&amp;rsquo;m sure those issues are
solvable, but I just wanted to get a profile up, not spend all my time solving lifetime issues!&lt;/p>
&lt;p>The other way to deal with this would be to write a macro of my own for the stats collection code.
But that would have the same code completion problems, and that feels way over-engineered. I think
what would be ideal would be a way to tell the &lt;code>graphql-client&lt;/code> codegen that these two types are the
same type. But that&amp;rsquo;s a PR for another day.&lt;/p>
&lt;aside>
&lt;small>
&lt;em>Edit 2022-04-02&lt;/em>: The &lt;em>other&lt;/em> other way was to write a set of &lt;code>impl From&amp;lt;...&amp;gt; for ...&lt;/code> traits to
convert from one type to another.
&lt;a href="https://github.com/autarch/autarch/blob/master/src/convert.rs">I ended up doing this&lt;/a> as I did more
work on my profile generator. Writing this code was tedious, and keeping it up to date will &lt;em>also&lt;/em>
be tedious, but the resulting code is a lot simpler than the other two approaches I considered.
Overall this is a win since the stats gathering code was the most complex code in this project and
not repeating it helps eliminate a lot of silly bugs.
&lt;/small>
&lt;/aside>
&lt;p>The final piece to make it all work is
&lt;a href="https://github.com/autarch/autarch/blob/master/.github/workflows/regenerate.yml">a simple GitHub Actions workflow that runs the generator&lt;/a>.
It runs whenever I push to the master branch of the generator repo as well as running nightly. If
the generated &lt;code>README.md&lt;/code> changes, it commits the changes and pushes the commit to the repo.&lt;/p>
&lt;p>GitHub will stop running scheduled workflows for a repo after 60 days of inactivity, but I&amp;rsquo;m fairly
sure that commits made by this workflow will count as activity, so it will never stop running. But I
will find out in a couple of months.&lt;/p>
&lt;p>If you want to use this for your own profile you&amp;rsquo;ll have to change some of the code. I have my
username and org as a constant in the code, and most people probably don&amp;rsquo;t have a single org for
most of their code. But really, you should probably use one of the many more battle-tested
alternatives.&lt;/p></description></item><item><title>Job Search 2022 Update: Week 3</title><link>https://blog.urth.org/2022/03/25/job-search-2022-update-week-3/</link><pubDate>Fri, 25 Mar 2022 19:28:14 -0500</pubDate><guid>https://blog.urth.org/2022/03/25/job-search-2022-update-week-3/</guid><description>&lt;p>Week 3 of my job search has come to a close, and boy is my brain tired! I&amp;rsquo;m looking forward to
starting a new job just so I can do something less exhausting than interviewing all week.&lt;/p>
&lt;p>I had a &lt;em>lot&lt;/em> of interviews (and a homework assignment) this week, but first, here&amp;rsquo;s where I stand
with various places &amp;hellip;&lt;/p>
&lt;p>First, the list of shame, which is those companies that have not responded after more than two
weeks:&lt;/p>
&lt;ul>
&lt;li>ClickHouse - applied on 2022-03-10&lt;/li>
&lt;li>GitHub - applied on 2022-03-10&lt;/li>
&lt;li>Netflix - applied on 2022-03-11&lt;/li>
&lt;li>Oso - applied on 2022-03-10&lt;/li>
&lt;/ul>
&lt;p>We can give ClickHouse and Oso a (partial?) pass here. They&amp;rsquo;re both startups and I can understand if
they don&amp;rsquo;t have a well-oiled hiring machine. But GitHub and Netflix really have no excuse. Is it
possible that no one has looked at my application yet? Or are they committing the gravest of all
hiring sins, ghosting people instead of sending rejection emails?&lt;/p>
&lt;p>I also submitted one more application. Kevin Centeno, who I greatly enjoyed working with at MaxMind,
reached out and said he had a friend working at Cockroach Labs who like it. So I went ahead and
applied on Monday, 2022-03-21. No word back yet.&lt;/p>
&lt;p>And here&amp;rsquo;s where I stand with all the other places I applied &amp;hellip;&lt;/p>
&lt;p>I heard back from 1Password! Of all the places I applied, this is the product I use the most (many
times every single day). So I&amp;rsquo;m quite excited about the idea of working on it. I have a first
interview scheduled for next week.&lt;/p>
&lt;p>For Array, I submitted my homework on 2022-03-23 and haven&amp;rsquo;t heard back yet. Either it was amazingly
bad or amazingly good. But the most likely cause is that everyone who could review it doing their
best to avoid reviewing yet another homework submission. I can understand that.&lt;/p>
&lt;p>I spent about 4 hours on this homework, which is more than I would have liked. The exercise was a
little too open-ended, in my opinion. I probably could have done less, but I didn&amp;rsquo;t want to turn in
work without any tests, even though they weren&amp;rsquo;t specifically asked for. I think the
&lt;a href="https://github.com/ActiveState/homework/tree/master/dep-tree">homework instructions I wrote at ActiveState&lt;/a>
are better at putting bounds on the amount of work required, but I wonder if we could have done ever
more there. Also, unlike ActiveState, there was no &amp;ldquo;point us at a GitHub project&amp;rdquo; alternative. This
is a bummer for me since I have literally 10s of thousands of lines of code on GitHub anyone can
look at. It might even be a hundred of thousands, but I haven&amp;rsquo;t counted.&lt;/p>
&lt;p>CircleCI said no, because of my desire to not get up early to talk to people in Europe. That&amp;rsquo;s very
reasonable.&lt;/p>
&lt;p>I talked to a recruiter at Fastly, and we agreed that it made the most sense to move me forward with
one of the three positions I applied for first. That means I have to talk to a different recruiter
first who handles that team. They reached out for my available times so hopefully that will happen
next week.&lt;/p>
&lt;p>LogDNA wins the Speedy Scheduling Award, as I got through all of their interviews this week. It
sounds like a good place to work, it&amp;rsquo;s a product space I understand, and I think I&amp;rsquo;d find the work
challenging and interesting. At the last interview, it sounded like an offer was likely and that I
should hear back next week. The downside of them being the speediest is that I will have to ask them
to wait a few weeks if they do make an offer, since I need to see what other offers come in. But
still, good job, LogDNA!&lt;/p>
&lt;p>I met with the Lead at MongoDB for the team I&amp;rsquo;d applied to and we did a live coding exercise. I know
I&amp;rsquo;ve said these are the worst in &lt;a href="https://blog.urth.org/2019/07/11/a-technical-hiring-process-revisited/">my writing on
hiring&lt;/a>, but ironically this
was fine for me. I didn&amp;rsquo;t find it too stressful because the task at hand was something very relevant
to my actual experience, not a I-will-never-do-this-at-work CS problem. Also, it was a collaborative
interaction, as opposed to someone just observing me. And finally, I was explicitly told to go ahead
and Google things (unlike my Google interview, more irony). So it went fine. I&amp;rsquo;m scheduled for
several more hours of interviews next Monday and Tuesday next week.&lt;/p>
&lt;p>I heard back from OneSignal and had a quick interview with the hiring manager for the position. It
sounds like they&amp;rsquo;re doing a lot of Rust, which is appealing. The interview went well, and they sent
me one of those &amp;ldquo;pick an interview slot&amp;rdquo; links from Lever. So I went and picked one for 4:00 PM, not
realizing that it was showing me US Pacific times! Oh, the irony. But seriously, Lever, maybe use
another thing I worked on, MaxMind&amp;rsquo;s
&lt;a href="https://www.maxmind.com/en/geoip2-precision-services">GeoIP APIs&lt;/a> and
&lt;a href="https://www.maxmind.com/en/geoip2-databases">databases&lt;/a>. They make it easy to use the visitor&amp;rsquo;s IP
to take an educated guess at the right time zone. And there&amp;rsquo;s no way to tell Lever I made a mistake
and want to reschedule. Fortunately, the hiring manager was able to resend the link and I scheduled
something at a better time. This will also be a live coding exercise. Hopefully, this will go as
well as the one with MongoDB did.&lt;/p>
&lt;p>A recruiter on LinkedIn reached out to me about a company called Oden and I had a first phone call
with the hiring manager today. Their product is all about collecting information on factory
production lines and providing insight for factory managers. This sounds fascinating. They said that
new hires get sent to a customer factory to see it in action. I think the only thing better would be
if this was software for giant killer robots. For the next step, they offer either a choice of
either homework or a live coding exercise. I chose the live coding because I don&amp;rsquo;t want to spend
another 4 hours on homework in a week that&amp;rsquo;s already starting to get packed with interviews.&lt;/p>
&lt;p>Onna asked me when I was available to meet with some folks from their team in Spain, asking if I was
available at 7:00 or 8:00 AM my time. That&amp;rsquo;s when I realized I hadn&amp;rsquo;t asked about work hours in
either of my first two conversations there. So I emailed the recruiter there and asked. They said
that they&amp;rsquo;d want me to be available every day from 9:00-12:00. Again, just like CircleCI, this is
totally reasonable, but I don&amp;rsquo;t want to do it. So I withdrew my application. I feel bad about this
because I should have asked the recruiter before I interviewed with their VP of Engineering.&lt;/p>
&lt;p>Finally, I spoke with the CEO of Optic. They have a pretty interesting product focused on making it
easy to review REST API changes in a similar way as we now review code changes. I have another
interview with an engineer there scheduled for next week.&lt;/p>
&lt;p>That&amp;rsquo;s it for job search updates. I&amp;rsquo;m still getting a lot of recruiter contacts on LinkedIn and some
emails too, but at this point I&amp;rsquo;m telling them I&amp;rsquo;m too busy for more interviews. I just checked and
saw a message from someone looking for &amp;ldquo;a Software Engineer experienced in Cobol, JCL, and SQL
Server Database to work on a contract basis&amp;rdquo;. Yes, my resume screams &amp;ldquo;this guy knows Cobol and JCL&amp;rdquo;.
Good job, recruiter!&lt;/p>
&lt;p>What will happen next week? Stay tuned.&lt;/p>
&lt;aside>
&lt;small>
Also, I&amp;rsquo;ve been fiddling around with a totally-pointless-because-a-dozen-alternatives-already-exist
tool in Rust to create
&lt;a href="https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/customizing-your-profile/managing-your-profile-readme">a GitHub profile README.md&lt;/a>
for myself. I&amp;rsquo;m planning to write a little bit about that soon, once I have it working.
&lt;/small>
&lt;/aside></description></item><item><title>Job Search 2022 Update: Week 2</title><link>https://blog.urth.org/2022/03/19/job-search-2022-update-week-2/</link><pubDate>Sat, 19 Mar 2022 14:42:29 -0500</pubDate><guid>https://blog.urth.org/2022/03/19/job-search-2022-update-week-2/</guid><description>&lt;p>Week 2 is over. My applications are moving forward in some places, and getting no response from some
places. So it&amp;rsquo;s exactly what I&amp;rsquo;d expect.&lt;/p>
&lt;p>Last Sunday, I was looking at my job search spreadsheet and realized I&amp;rsquo;d somehow deleted 1Password,
so I quickly put in an application there. That brings my application total to 18, plus I&amp;rsquo;ve had some
sort of informal talks with a few other places.&lt;/p>
&lt;p>Of the places I&amp;rsquo;ve applied, the following companies simply haven&amp;rsquo;t responded yet (excluding the
automated email they send to all applicants):&lt;/p>
&lt;ul>
&lt;li>1Password&lt;/li>
&lt;li>ClickHouse&lt;/li>
&lt;li>Grafana&lt;/li>
&lt;li>GitHub&lt;/li>
&lt;li>Netflix&lt;/li>
&lt;li>OneSignal&lt;/li>
&lt;li>Oso&lt;/li>
&lt;/ul>
&lt;p>Now, GitHub and Netflix don&amp;rsquo;t surprise me. They&amp;rsquo;re big, desirable places to work that probably
receive a huge number of applications. Much like Google, I imagine that their problem is dealing
with the flood, as opposed to getting enough good applicants. I do have an inside person at Netflix
who said they&amp;rsquo;d put in a good word for me, but still no response.&lt;/p>
&lt;p>But are the others on this list in the same situation? Maybe, I don&amp;rsquo;t know. That said, it&amp;rsquo;s annoying
to get &lt;em>no&lt;/em> response for a week or more. When I have done hiring for past employers I&amp;rsquo;ve always
tried to respond to applicants within a few days.&lt;/p>
&lt;p>Fastly was also slow to reply, but my friend Emily who works there was able to poke someone. After
that, they asked me to give my availability, so hopefully I&amp;rsquo;ll move forward with them next week.&lt;/p>
&lt;p>Discord rejected my application. I don&amp;rsquo;t know why, of course. The application form does have a
yes/no question asking &amp;ldquo;If applicable, would you be willing to relocate to Discord&amp;rsquo;s SF HQ? While
Discord is embracing a hybrid remote approach going forward, some roles will remain HQ-based.&amp;rdquo; I
answered &amp;ldquo;no&amp;rdquo;.&lt;/p>
&lt;p>I had applied for an engineering manager role there, so maybe they would only accept someone who
said &amp;ldquo;yes&amp;rdquo; here? If that&amp;rsquo;s the case, it would be nice if they said that in the job description. But
of course, they may have rejected my application for some other reason too.&lt;/p>
&lt;p>I also had a bunch of interviews:&lt;/p>
&lt;ul>
&lt;li>MongoDB - first interview&lt;/li>
&lt;li>Onna - first interview&lt;/li>
&lt;li>Array - second interview&lt;/li>
&lt;li>CircleCI - first interview&lt;/li>
&lt;/ul>
&lt;p>LogDNA wins the award for moving the quickest! Last week I talked to their recruiter, and this week
I interviewed with the hiring manager and then had a technical interview and design challenge
interview this week.&lt;/p>
&lt;p>Next week I have a few more interviews, including the next steps with MongoDB and Onna.&lt;/p>
&lt;p>I also had a call with the VP of Engineering at APFusion. This was more of an exploratory chat,
though of course &lt;em>every&lt;/em> interaction is an interview, since either side can always decide to not
move forward.&lt;/p>
&lt;p>When I started my search, I updated my profile on Y Combinator&amp;rsquo;s
&lt;a href="https://www.workatastartup.com/">Work at a Startup site&lt;/a>. This has job postings for a &lt;em>lot&lt;/em> of
startups of all sizes, and companies can see your profile if you want. Two people reached out to me,
and I&amp;rsquo;m scheduled to talk to the CEO of Optic next week. Another company also reached out, but it
was a product I wasn&amp;rsquo;t interested in, so I politely declined.&lt;/p>
&lt;h2 id="salary-and-negotiation">Salary and Negotiation&lt;/h2>
&lt;p>I&amp;rsquo;ve so far resisted every attempt to get me to provide a salary I&amp;rsquo;d want. I usually ask if they
have a range. If they tell me, I can say yes, let&amp;rsquo;s move forward. In one case I said I&amp;rsquo;d probably
want a bit more than the upper end of their range, though they said the upper end had some
flexibility.&lt;/p>
&lt;p>Everything I&amp;rsquo;ve read on negotiation says &amp;ldquo;don&amp;rsquo;t give a minimum or a range that you&amp;rsquo;d accept.&amp;rdquo; One
blog post I read suggested saying something like &amp;ldquo;I&amp;rsquo;d prefer to discuss that later in the process
after we discover if this is a good fit.&amp;rdquo; I&amp;rsquo;ve tried saying that in some cases but it feels pretty
inauthentic for me. I don&amp;rsquo;t like telling someone a lie that they know is a lie just because for some
reason it&amp;rsquo;s considered crass to speak the thing that we both know is the truth. So I&amp;rsquo;ve moved to
saying &amp;ldquo;I won&amp;rsquo;t give you a number no matter how much you say you need one because that would be poor
negotiation on my part.&amp;rdquo; But no one I&amp;rsquo;ve said that to has seemed upset or offended.&lt;/p>
&lt;p>For the record, here are some of the posts/articles I&amp;rsquo;ve read on negotiation:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.kalzumeus.com/2012/01/23/salary-negotiation/">Salary Negotiation: Make More Money, Be More Valued by Patrick McKenzie (aka patio11)&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://fearlesssalarynegotiation.com/salary-expectations-interview-question/">Salary expectations questions - How should you answer them?&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://haseebq.com/my-ten-rules-for-negotiating-a-job-offer/">Ten Rules for Negotiating a Job Offer by Haseeb&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://haseebq.com/how-not-to-bomb-your-offer-negotiation/">How Not to Bomb Your Offer Negotiation by Haseeb&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>My number one takeaway from all this was that by the time the company has made you an offer they&amp;rsquo;re
already quite invested in hiring you. Another important takeaway is that the value of the money to
you versus the value to the company is very different. Getting an extra $10,000 or $20,000 (or more)
in salary, hiring bonus, or RSUs is a drop in the bucket for all but the tiniest companies, but it
has a huge impact on you.&lt;/p>
&lt;p>So don&amp;rsquo;t be afraid to ask for more. Unless you do something ridiculous like asking for 4x what was
offered, it&amp;rsquo;s &lt;em>very&lt;/em> unlikely that they will simply pull the offer. The worst case is that they say
&amp;ldquo;sorry, we can&amp;rsquo;t offer any more.&amp;rdquo;&lt;/p>
&lt;h2 id="whats-next">What&amp;rsquo;s Next?&lt;/h2>
&lt;p>I don&amp;rsquo;t plan to submit more applications at the moment. I think I have some solid prospects based on
my interviews so far. I&amp;rsquo;ll see how things go this week and if I get a lot more rejections, I&amp;rsquo;ll
probably end up applying to more places. But even managing the number of responses I&amp;rsquo;ve had so far
is a lot to deal with. Interviews are exhausting!&lt;/p></description></item><item><title>Job Search 2022 Update: Week 1.1</title><link>https://blog.urth.org/2022/03/14/job-search-2022-update-week-1-1/</link><pubDate>Mon, 14 Mar 2022 16:00:45 -0500</pubDate><guid>https://blog.urth.org/2022/03/14/job-search-2022-update-week-1-1/</guid><description>&lt;p>So after &lt;a href="https://blog.urth.org/2022/03/11/job-search-2022-update-week-1/">my last update&lt;/a>, I had one small change to write about.&lt;/p>
&lt;p>For reference, I live in Minneapolis, which means I&amp;rsquo;m in America/Chicago (US Central). I&amp;rsquo;d been
talking to an (external) recruiter about positions at Namely. The recruiter told me they asked, &amp;ldquo;Are
you willing to work east coast hours?&amp;rdquo;.&lt;/p>
&lt;p>I asked what that meant, since I&amp;rsquo;m very much not a morning person, and I&amp;rsquo;m not up to working 8-4 my
time. I&amp;rsquo;ve had jobs where I had to get up early, and I was &lt;em>never&lt;/em> able to shift my sleep times
enough. I&amp;rsquo;d always stay up too late and I&amp;rsquo;d be tired most of the week, which was unpleasant. One of
the best things about working for mostly remote companies has been flexibility in regards to working
hours, with most places expecting that there would be some sort of 3-5 hour &amp;ldquo;core hours&amp;rdquo; block that
mapped to late morning/early afternoon for most US time zones.&lt;/p>
&lt;p>So then the recruiter said they wanted to know when I could start. I said 10 am. Most days I&amp;rsquo;m up
before this, but occasionally I do sleep later than this, so I don&amp;rsquo;t love the idea of committing to
10 am every day. But it&amp;rsquo;s not untenable, just something I&amp;rsquo;d factor into my decision-making.&lt;/p>
&lt;p>But this was too late for Namely so they didn&amp;rsquo;t want to move forward.&lt;/p>
&lt;p>Maybe it&amp;rsquo;s just me but this seemed strange, especially for a company that lists all of its jobs as
having remote options. If 11 am east coast time is too late, does that mean they won&amp;rsquo;t hire anyone
on the west coast unless they can start before 8 am west coast time every day? Even people who
normally get up early may not be able to make that work, especially if they have kids.&lt;/p>
&lt;p>I do want to emphasize that I have no complaints about the recruiter who was acting as a relay in
all this. He was quick to respond to me and was just passing on what the company was asking.&lt;/p>
&lt;p>Overall, this is also a win. It&amp;rsquo;s always good to find out these types of issues as early as
possible, before either side has invested much time in the process.&lt;/p></description></item><item><title>Job Search 2022 Update: Week 1</title><link>https://blog.urth.org/2022/03/11/job-search-2022-update-week-1/</link><pubDate>Fri, 11 Mar 2022 14:27:04 -0600</pubDate><guid>https://blog.urth.org/2022/03/11/job-search-2022-update-week-1/</guid><description>&lt;p>Week 1 of my job search is coming to a close. Here&amp;rsquo;s an update on what I&amp;rsquo;ve been up to and where I&amp;rsquo;m
at.&lt;/p>
&lt;aside>
&lt;small>
See &lt;a href="https://blog.urth.org/2022/03/07/let-the-job-search-begin-and-my-first-interviews/">my first job search post&lt;/a> for a TLDR of what I&amp;rsquo;m looking
for. Please &lt;a href="mailto:autarch@urth.org">let me know&lt;/a> if you have an opportunity that fits!
&lt;/small>
&lt;/aside>
&lt;p>First off, I&amp;rsquo;d like to thank the people who&amp;rsquo;ve reached out to me in various ways since my first
post, as well as people I&amp;rsquo;ve contacted for the inside scoop at various places:&lt;/p>
&lt;ul>
&lt;li>Adam Reinhardt&lt;/li>
&lt;li>Andrea Gunn&lt;/li>
&lt;li>Andrew Cholakian&lt;/li>
&lt;li>David Golden&lt;/li>
&lt;li>Dylan Martin&lt;/li>
&lt;li>Emily Shea&lt;/li>
&lt;li>Heidi Gider&lt;/li>
&lt;li>Joelle Maslak&lt;/li>
&lt;li>Jordan Adler&lt;/li>
&lt;li>Karen Etheridge&lt;/li>
&lt;li>Konstantin Narkhov&lt;/li>
&lt;li>Mark Fowler&lt;/li>
&lt;li>Miguel Mateus&lt;/li>
&lt;li>Pete Sergeant&lt;/li>
&lt;li>Ryan Gerry&lt;/li>
&lt;li>Tatsuhiko Miyagawa&lt;/li>
&lt;li>Neil Bowers&lt;/li>
&lt;li>If I forgot to put your name here let me know, and I&amp;rsquo;m sorry!&lt;/li>
&lt;/ul>
&lt;p>Several of these folks have submitted me as an internal referral, which is always a huge boost for
an applicant.&lt;/p>
&lt;aside>
&lt;small>
I had a bit of a rant about recruiters here, but this was getting long. I may release that as a DLC
for this post.
&lt;/small>
&lt;/aside>
&lt;p>I started off the week by looking through a &lt;em>very&lt;/em> long list of bookmarks I&amp;rsquo;d been collecting since
I left my last job in October. This was mostly companies that used Rust or had 4-day weeks or for
some other reason had caught my eye. This list came from &amp;ldquo;Who&amp;rsquo;s Hiring&amp;rdquo; posts on Hacker News and the
Rust subreddit, as well as looking at various job boards and my own research.&lt;/p>
&lt;p>I first made a spreadsheet to track the status of all my applications. I borrowed from
&lt;a href="https://philcalcado.com/2021/12/20/job_hunt.html">the one described by Phil Calçado on his blog&lt;/a>.
Here&amp;rsquo;s
&lt;a href="https://docs.google.com/spreadsheets/d/13N6onx-0fGP6ahoQt1XmhAFI7Xr-4KIkFqDGXthw6T8/edit?usp=sharing">an example version of the Google Sheet I&amp;rsquo;m using&lt;/a>.
Feel free to make a copy of it for your own search.&lt;/p>
&lt;link rel="stylesheet" href="https://blog.urth.org/css/hugo-easy-gallery.css" />
&lt;div class="box">
&lt;figure itemprop="associatedMedia"
itemscope itemtype="http://schema.org/ImageObject" >
&lt;div class="img">
&lt;img itemprop="thumbnail" src="https://blog.urth.org/image/2022-03-job-search-spreadsheet.png" alt="An example of my job tracking spreadsheet"/>
&lt;/div>
&lt;a href="https://blog.urth.org/image/2022-03-job-search-spreadsheet.png" itemprop="contentUrl">&lt;/a>
&lt;/figure>
&lt;/div>
&lt;p>As of right now, I have 93 entries in my spreadsheet. That&amp;rsquo;s clearly too many to apply to.&lt;/p>
&lt;p>I ended up filtering based on the &amp;ldquo;My Interest&amp;rdquo;. If a company seemed promising but didn&amp;rsquo;t have any
open roles that fit me, I marked them as a 0. They might be a fit in the future. I also marked a few
companies as -1, which means I should probably not consider them in the future. The reasons for that
include:&lt;/p>
&lt;ul>
&lt;li>I applied and was rejected (just Google right now).&lt;/li>
&lt;li>They cannot hire in Minnesota (Bolt).&lt;/li>
&lt;li>Their published salaries are &lt;em>way&lt;/em> below my desired range.&lt;/li>
&lt;li>They are in a field I don&amp;rsquo;t want to be in, like cryptocurrency.&lt;/li>
&lt;li>They expect &amp;gt; 40 hours per week of work (Wallaroo).&lt;/li>
&lt;li>They exclude Colorado or NYC residents from applying for positions&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.&lt;/li>
&lt;li>Someone works there that I don&amp;rsquo;t want to work with&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>.&lt;/li>
&lt;/ul>
&lt;p>That left me with a range of 2-4 for the remaining companies. I started by giving a 2 or a 3 to any
company that seemed particularly appealing because:&lt;/p>
&lt;ul>
&lt;li>I use and like their product (Discord, GitHub, Netflix, and others).&lt;/li>
&lt;li>The position seemed particularly interesting (for example, Grafana Labs has a position working on
OpenTelemetry that I&amp;rsquo;m intrigued by).&lt;/li>
&lt;li>They use Rust (Fastly, OneSignal, Oso, and others).&lt;/li>
&lt;li>They have a 4-day week or might be willing to offer me one.&lt;/li>
&lt;li>I know someone who works there and they tell me it&amp;rsquo;s a good place to work (Elastic, Fastly,
MongoDB, and others).&lt;/li>
&lt;li>They are known to pay obscene amounts of money (Netflix and others).&lt;/li>
&lt;/ul>
&lt;p>I&amp;rsquo;ve applied or had recruiters submit me for 17 companies so far. Those companies are:&lt;/p>
&lt;ul>
&lt;li>Array&lt;/li>
&lt;li>CircleCI&lt;/li>
&lt;li>ClickHouse&lt;/li>
&lt;li>Discord&lt;/li>
&lt;li>Elastic&lt;/li>
&lt;li>Fastly&lt;/li>
&lt;li>GitHub&lt;/li>
&lt;li>Google&lt;/li>
&lt;li>Grafana Labs&lt;/li>
&lt;li>LogDNA&lt;/li>
&lt;li>MongoDB&lt;/li>
&lt;li>Namely&lt;/li>
&lt;li>Netflix&lt;/li>
&lt;li>OneSignal&lt;/li>
&lt;li>Onna&lt;/li>
&lt;li>Oso&lt;/li>
&lt;li>Wallaroo&lt;/li>
&lt;/ul>
&lt;p>I was really excited about the position at Elastic I applied for, but it was already filled&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>. I
talked to a recruiter from a different part of the company, and they were very interested in me,
which felt nice!&lt;/p>
&lt;p>But the positions were on all-Java teams, involved very little coding, and I think they want someone
with more cloud deployment and storage expertise than I have. There weren&amp;rsquo;t any other positions at
my level that felt like a good fit for me, so I decided not to move forward with Elastic. This is
too bad. My good friend Andrew works there and I&amp;rsquo;d love to work with him, plus he&amp;rsquo;s told me it&amp;rsquo;s a
great place to work.&lt;/p>
&lt;p>Of the remainder, I talked to one earlier today and I&amp;rsquo;m scheduled to talk to four next week, so that
feels like good progress.&lt;/p>
&lt;p>I&amp;rsquo;ll try to write weekly updates as I go forward, so stay tuned.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>But I&amp;rsquo;m in Minnesota, right? However, the reason they exclude Colorado residents is that
Colorado requires all job postings to include a salary range. NYC also has a similar law coming
into effect on May 15. I also saw Home Depot excludes California, presumably because California
has very strong anti-non-compete and other labor laws. I don&amp;rsquo;t want to work for a company that
sees those laws as a bad thing.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Without naming names I&amp;rsquo;ll note that some people in the FOSS world have a reputation for being
&amp;ldquo;difficult&amp;rdquo;.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>They told me it was filled on Tuesday but it&amp;rsquo;s still listed on their careers page. I could go on
at great length about how bad various companies&amp;rsquo; career pages are and how bad every applicant
tracking system is.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Let the Job Search Begin; And My First Interviews</title><link>https://blog.urth.org/2022/03/07/let-the-job-search-begin-and-my-first-interviews/</link><pubDate>Mon, 07 Mar 2022 14:16:04 -0600</pubDate><guid>https://blog.urth.org/2022/03/07/let-the-job-search-begin-and-my-first-interviews/</guid><description>&lt;p>So it&amp;rsquo;s that time of my life again. The time when a not-so-young&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> man has to get a job because he
can&amp;rsquo;t live off his savings forever. And that not-so-young man is me.&lt;/p>
&lt;p>If you know of something that might be a good fit for me please
&lt;a href="mailto:autarch@urth.org">let me know&lt;/a>.&lt;/p>
&lt;p>I wrote up a TLDR I&amp;rsquo;ve been sharing with recruiters that will help you figure out if something is a
good fit for me.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Level:&lt;/strong> Senior+ (Lead, Staff, Principal), or Senior at FAANGMULA (or equivalent very large tech
company like Stripe). Would also consider Senior for a Rust job.&lt;/li>
&lt;li>&lt;strong>Management:&lt;/strong> Yes, if there is also IC work.&lt;/li>
&lt;li>&lt;strong>Fields:&lt;/strong> Tech/software, but &lt;strong>no crypto/blockchain, surveillance, defense industry, or gig
economy services for non-professionals&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Technologies:&lt;/strong>
&lt;ol>
&lt;li>Rust&lt;/li>
&lt;li>Something unusual, e.g. Elixir, Nim, etc.&lt;/li>
&lt;li>Go&lt;/li>
&lt;li>Anything else, but &lt;em>no Java or PHP&lt;/em>&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>&lt;strong>Location:&lt;/strong> 100% remote&lt;/li>
&lt;li>&lt;strong>Comp:&lt;/strong> Top tier TC or 4 day (32 hour) week - or both!&lt;/li>
&lt;li>&lt;strong>Hours:&lt;/strong> No more than 40 hours per week (no startup 45-50 hours stuff)&lt;/li>
&lt;li>&lt;strong>PTO:&lt;/strong> 5+ weeks&lt;/li>
&lt;/ul>
&lt;p>There are a lot more things I care about, but if I included all of them this would be the &amp;ldquo;too
long&amp;rdquo;, not the TLDR.&lt;/p>
&lt;hr>
&lt;p>I think I&amp;rsquo;ll also blog a bit about my interviews as they happen. Why not? I plan to be circumspect
about some things, especially specific technical questions and coding exercises. It&amp;rsquo;s not
appropriate for me to share those with the world.&lt;/p>
&lt;p>So far I&amp;rsquo;ve had two interviews.&lt;/p>
&lt;p>My first was last Wednesday (March 2) with &lt;a href="https://www.wallaroo.ai/">Wallaroo&lt;/a>. This went well &amp;hellip;
up until the interviewer told me that they expect people to work 45-50 hours per week. So I withdrew
my application immediately.&lt;/p>
&lt;p>I consider this interview a huge success. We found out that this wasn&amp;rsquo;t a fit in under 30 minutes,
before people on either side of the process had invested much time. That&amp;rsquo;s a win in my book!&lt;/p>
&lt;p>My second was earlier today with Google&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>. It was a phone screening with a shared virtual doc
(sort of like Google Docs but it defaulted to monospace at least). The question was comp sci-ish,
and I think I did pretty terribly. I dove in and started implementing this in a not-very-optimal
way. After a while, the interviewer asked me &amp;ldquo;could you implement this using X&amp;rdquo;, where X is a &lt;em>very&lt;/em>
basic programming concept that I know quite well. And my response was along the lines of &amp;ldquo;oh yes, X
is definitely the right way to do this&amp;rdquo;. But at that point we were mostly out of time so I couldn&amp;rsquo;t
redo my work using X.&lt;/p>
&lt;p>I&amp;rsquo;ve already &lt;a href="https://blog.urth.org/2019/07/11/a-technical-hiring-process-revisited/">written a fair bit about interviewing&lt;/a> and the issues with live coding exercises.
If I&amp;rsquo;d been doing this on my own with less time pressure, I think I would&amp;rsquo;ve hit on X in a
reasonable time frame. But in a live 45-minute window it felt like either I picked the right
approach immediately or I failed. I don&amp;rsquo;t think that&amp;rsquo;s a great screening tool, since it has a lot of
false negatives. But the top employers like Google do just fine with a lot of false negatives, so it
makes sense that they&amp;rsquo;d stick with this approach.&lt;/p>
&lt;p>Now, I don&amp;rsquo;t &lt;em>know&lt;/em> that I failed (yet), but I don&amp;rsquo;t think I made a great impression.&lt;/p>
&lt;p>&lt;strong>Edit @ 14:45pm my time: They chose not to move forward. No surprise. But it&amp;rsquo;s great that they&amp;rsquo;re
so quick to follow up.&lt;/strong>&lt;/p>
&lt;p>And that&amp;rsquo;s where I&amp;rsquo;m at. I have a long list of bookmarks for companies that I might want to apply
at, so I&amp;rsquo;m going through those now and figuring out where to apply first.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>But not so old either!&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>The recruiter I talked to swears that Google now has fully remote roles. I would want to confirm
this before doing any onsite interviews, because I have no interest in moving for a job.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Checking Tailwind Class Names at Compile Time with Rust</title><link>https://blog.urth.org/2022/02/21/checking-tailwind-class-names-at-compile-time-with-rust/</link><pubDate>Mon, 21 Feb 2022 10:07:18 -0600</pubDate><guid>https://blog.urth.org/2022/02/21/checking-tailwind-class-names-at-compile-time-with-rust/</guid><description>&lt;p>At the end of &lt;a href="https://blog.urth.org/2022/02/14/frontend-rust-without-node/">my last post, &amp;ldquo;Frontend Rust Without Node&amp;rdquo;&lt;/a>, I talked about my big issue with using
&lt;a href="https://tailwindcss.com/">Tailwind CSS&lt;/a>. &lt;strong>It has a huge number of classes, I can&amp;rsquo;t remember their
names, so I often typed them incorrectly. This made it difficult to figure out why my styling wasn&amp;rsquo;t
doing what I thought.&lt;/strong>&lt;/p>
&lt;aside>
&lt;small>
&lt;strong>TLDR&lt;/strong>: Don&amp;rsquo;t care about how any of this works, but just want to use the tooling I
wrote? &lt;a href="https://blog.urth.org/#putting-it-all-together">Jump to the end and ignore all my blather&lt;/a>.
&lt;/small>
&lt;/aside>
&lt;p>Here&amp;rsquo;s a recap of &lt;em>why&lt;/em> this is the case &amp;hellip;&lt;/p>
&lt;p>Tailwind consists of class names and &amp;ldquo;modifiers&amp;rdquo;. A class name is something like &lt;code>text-lg&lt;/code> or
&lt;code>grid&lt;/code>. Any class name can also have a variety of modifiers attached to it, separated by colons, so
you can write something like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;hidden lg:visible hover:background-indigo-200&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Content that will only be visible above a certain screen size.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>You can also combine modifiers to create classes like &lt;code>lg:hover:background-indigo-200&lt;/code>. So while
there are &amp;ldquo;only&amp;rdquo; a few hundred CSS class names, the number of names you can use is
&lt;code>&amp;quot;base&amp;quot; names × modifiers!&lt;/code> (that&amp;rsquo;s a factorial sign on &lt;code>modifiers!&lt;/code>). It&amp;rsquo;s not &lt;em>really&lt;/em> a factorial
since you can&amp;rsquo;t combine every modifier with every other modifier (&lt;code>sm:md:lg:visible&lt;/code> makes no
sense), but it&amp;rsquo;s a lot more than a simple multiplication.&lt;/p>
&lt;p>As such, it&amp;rsquo;s not practical to simply generate a CSS file with all possible classes. Well, I lied.
It&amp;rsquo;s entirely practical because it&amp;rsquo;s trivially doable. Just add this to your &lt;code>tailwind.config.js&lt;/code>
&amp;hellip;&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">content&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">safelist&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">pattern&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sr">/.*/&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">variants&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span> &lt;span class="s2">&amp;#34;sm&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;md&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;lg&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;xl&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&amp;hellip; and then run the &lt;code>tailwindcss&lt;/code> program to generate the CSS file&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>. When I tried this, without
even including all variants, I ended up with a 7MB CSS file and I&amp;rsquo;d only be using a tiny fraction of
what it contained.&lt;/p>
&lt;p>So it&amp;rsquo;s not a good idea, and it&amp;rsquo;s not how the Tailwind CSS authors intend Tailwind to be used.
Instead, the &lt;code>tailwindcss&lt;/code> program will scan your code with some broad regexes to find strings that
&lt;em>could be&lt;/em> Tailwind CSS class names. Then it generates a CSS file containing just those strings
which match actual Tailwind names.&lt;/p>
&lt;p>But this scanning process, because it matches so broadly, errs on the side of false positives, and
the &lt;code>tailwindcss&lt;/code> program will not emit any warnings when it finds a string that &lt;em>could&lt;/em> be a match,
but which isn&amp;rsquo;t. If it did that, you&amp;rsquo;d end up with hundreds or thousands of warnings quite quickly,
as it could match nearly every variable and function name in your codebase, depending on your naming
conventions.&lt;/p>
&lt;p>So in my first attempts to use Tailwind with Dioxus, my workflow ended up like this:&lt;/p>
&lt;ul>
&lt;li>Add some class names in my
&lt;a href="https://reactjs.org/">React&lt;/a>/&lt;a href="https://dioxuslabs.com/">Dioxus&lt;/a>/&lt;a href="https://seed-rs.org/">Seed&lt;/a>/&lt;a href="https://yew.rs/">Yew&lt;/a>
code.&lt;/li>
&lt;li>Re-run &lt;code>tailwindcss&lt;/code> against my code base to regenerate my &amp;ldquo;compiled&amp;rdquo; CSS file. &lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>&lt;/li>
&lt;li>Look at my app, which since I&amp;rsquo;m using &lt;a href="https://trunkrs.dev/">Trunk&lt;/a> will have hot-reloaded in my
browser.&lt;/li>
&lt;li>Scream into the void when my CSS changes did not do what I intended, usually doing absolutely
nothing. Then try to debug what happened by asking:
&lt;ul>
&lt;li>Did my CSS actually get regenerated or is my Trunk config not doing what I think it should do?&lt;/li>
&lt;li>Does the regenerated CSS contain the new class names?
&lt;ul>
&lt;li>If yes &amp;hellip;
&lt;ul>
&lt;li>Did the browser properly load the new CSS file&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>?&lt;/li>
&lt;li>Does the CSS do what I think it does when attached to the element I think I attached it?&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>If no &amp;hellip;
&lt;ul>
&lt;li>Did the &lt;code>tailwindcss&lt;/code> work as I expected it to or did I screw up its config so it didn&amp;rsquo;t see
the class names I just added?&lt;/li>
&lt;li>&lt;strong>Or did I typo a class name for the thousandth time today?&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>I spent a &lt;em>lot&lt;/em> of time asking these questions. And most of the time, I had typoed a Tailwind class
name but nothing in my toolchain was telling me I had done so.&lt;/p>
&lt;p>This was annoying.&lt;/p>
&lt;p>It was doubly annoying because I&amp;rsquo;m using Rust. If Rust does one thing well, that thing is telling me
at compile time all the many things I did wrong.&lt;/p>
&lt;h2 id="enlisting-the-rust-compiler-to-check-my-css">Enlisting the Rust Compiler to Check my CSS&lt;/h2>
&lt;p>Fortunately, I knew I could make the Rust compiler check this for me. When I experimented with Seed
before Dioxus, &lt;a href="https://github.com/seed-rs/seed-quickstart-webpack">the quickstart template I used&lt;/a>
included a plugin for &lt;a href="https://postcss.org/">PostCSS&lt;/a> written by
&lt;a href="https://github.com/MartinKavik">Martin Kavik&lt;/a>,
&lt;a href="https://www.npmjs.com/package/postcss-typed-css-classes">postcss-typed-css-classes&lt;/a>, that hooked
into PostCSS and generated Rust code for all of its classes&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>But I didn&amp;rsquo;t want to use that plugin for a couple of reasons:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>I had so far managed to avoid needing to run &lt;code>node&lt;/code> for my project, so I didn&amp;rsquo;t want to use
PostCSS, which requires &lt;code>node&lt;/code>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>The code generated by that PostCSS puts &lt;em>all&lt;/em> of the classes, tens of thousands of them, into a
single struct in an 8MB file. The reason it&amp;rsquo;s so large is because it includes a huge number of
&lt;code>class × modifiers&lt;/code>, and it doesn&amp;rsquo;t even come class to including all possible modifier/class
combinations.&lt;/p>
&lt;p>This &lt;em>killed&lt;/em> my editor&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup> when it came to auto-completion. Even loading the generated file in
my editor is slow, probably because of syntax highlighting. And jumping around the file or
searching in it is also quite slow.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>Obviously, #2 is fixable, but to fix #1 I needed a new tool, ideally written in Rust, since that&amp;rsquo;s
what everything else I&amp;rsquo;m using is in.&lt;/p>
&lt;h2 id="so-i-wrote-that-new-tool">So I Wrote That New Tool&lt;/h2>
&lt;p>It&amp;rsquo;s called &lt;a href="https://lib.rs/crates/tailwindcss-to-rust">&lt;code>tailwindcss-to-rust&lt;/code>&lt;/a>. It generates Rust
code with &lt;em>all&lt;/em> of the available Tailwind CSS class names and modifiers as static strings. It
&lt;em>doesn&amp;rsquo;t&lt;/em> generate strings for modifier/class combinations, which means that the full file is only
624kb. That&amp;rsquo;s still pretty big, but an order of magnitude smaller than the one generated by the
PostCSS plugin. My editor takes a slight pause when it loads, but it&amp;rsquo;s only a second or two. And
jumping around the file and searching it is quick enough to feel instantaneous.&lt;/p>
&lt;p>And to further speed up code completion, I split up the classes into a set of structs, where each
struct represents a &amp;ldquo;group&amp;rdquo; of classes based on function (layout, typography, animation, etc.).
These groups are taken from &lt;a href="https://tailwindcss.com/docs/">the Tailwind documentation&lt;/a> headings.&lt;/p>
&lt;p>Unfortunately, there&amp;rsquo;s nothing in the Tailwind codebase to make this easier. There&amp;rsquo;s no list of all
the available class names, and there&amp;rsquo;s no reference to the documentation groups in the codebase at
all. So all the information I needed, the group and class names, only exists in the documentation or
in a generated CSS file. And to make it even worse, the documentation itself is entirely generated
by code.&lt;/p>
&lt;p>Fortunately, as an old school Perl hacker, I know how to whip up some horrible hacks, sanity be
damned! I wrote
&lt;a href="https://github.com/houseabsolute/tailwindcss-to-rust/blob/master/dev/bin/get-categories.pl">a Perl script&lt;/a>&lt;sup id="fnref:6">&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref">6&lt;/a>&lt;/sup>
that crawls the Tailwind documentation site and generates a Rust data structure mapping individual
class names to groups.&lt;/p>
&lt;p>If you&amp;rsquo;re running in terror, don&amp;rsquo;t worry, &lt;strong>you don&amp;rsquo;t need to use Perl to use the
&lt;code>tailwindcss-to-rust&lt;/code> tool&lt;/strong>. I wrote the Perl to help me write the Rust to generate the Rust. And
you just need to run the Rust that generates the Rust, not the Perl that generates (some of) the
Rust to generate the Rust. I hope that clears things up.&lt;/p>
&lt;p>The &lt;em>actual&lt;/em> generator, &lt;code>tailwindcss-to-rust&lt;/code> (written in Rust) takes as its input your
&lt;code>tailwind.config.js&lt;/code> file and an input CSS file for the &lt;code>tailwindcss&lt;/code> program. We&amp;rsquo;ll call that input
file &lt;code>tailwind.css&lt;/code> for this explanation. This input file is usually just a few lines, see
&lt;a href="https://tailwindcss.com/docs/installation">step 3 of the Tailwind installation docs&lt;/a> for details.
Then the generator does the following:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>Creates a temp directory.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Copies your &lt;code>tailwind.config.js&lt;/code> and input CSS file to the temp dir.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Adds &lt;code>safelist: [ { pattern: /.*/ } ]&lt;/code> to the &lt;code>tailwind.config.js&lt;/code> in the temp dir.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>If the directory containing the given &lt;code>tailwind.config.js&lt;/code> file contains a &lt;code>node_modules&lt;/code>
directory, that directory is symlinked from the temp directory. This is so it can access any
tailwind plugins in that directory. I&amp;rsquo;m honestly not sure if this achieves anything, but I haven&amp;rsquo;t
experimented with any plugins that don&amp;rsquo;t ship as part of the &lt;code>tailwindcss&lt;/code> binary.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Runs &lt;code>tailwindcss&lt;/code> in the temp directory, using the modified config file. Because it added that
&lt;code>safelist&lt;/code> item to the config, the generated file will include &lt;em>every&lt;/em> possible CSS class. The
exact classes vary based on what Tailwind plugins you are using.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&amp;ldquo;Parses&amp;rdquo; the generated CSS file to find all the class names it contains.&lt;sup id="fnref:7">&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref">7&lt;/a>&lt;/sup>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Generates Rust code with structs for all of those classes. If there are class names the generator
doesn&amp;rsquo;t recognize then they are put in a struct named &amp;ldquo;Unknown&amp;rdquo;.&lt;/p>
&lt;p>In the future, I may add an option to provide a group mapping for class names. If this tool sees
broader adoption I&amp;rsquo;m sure people will want this, because one of the most powerful features of
Tailwind is that you can quite easily create custom classes and modifiers.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>The generated Rust code looks like this&lt;sup id="fnref:8">&lt;a href="#fn:8" class="footnote-ref" role="doc-noteref">8&lt;/a>&lt;/sup>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#[derive(Clone, Copy)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">struct&lt;/span> &lt;span class="nc">Modifiers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">active&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="nb">&amp;#39;static&lt;/span> &lt;span class="kt">str&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">after&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="nb">&amp;#39;static&lt;/span> &lt;span class="kt">str&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">lg&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="nb">&amp;#39;static&lt;/span> &lt;span class="kt">str&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ltr&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="nb">&amp;#39;static&lt;/span> &lt;span class="kt">str&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">visited&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="nb">&amp;#39;static&lt;/span> &lt;span class="kt">str&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">xl&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="nb">&amp;#39;static&lt;/span> &lt;span class="kt">str&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">const&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">M&lt;/span>: &lt;span class="nc">Modifiers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Modifiers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">active&lt;/span>: &lt;span class="s">&amp;#34;active&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">after&lt;/span>: &lt;span class="s">&amp;#34;after&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">lg&lt;/span>: &lt;span class="s">&amp;#34;lg&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">ltr&lt;/span>: &lt;span class="s">&amp;#34;ltr&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">visited&lt;/span>: &lt;span class="s">&amp;#34;visited&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">xl&lt;/span>: &lt;span class="s">&amp;#34;xl&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="cp">#[derive(Clone, Copy)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">struct&lt;/span> &lt;span class="nc">Accessibility&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">not_sr_only&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="nb">&amp;#39;static&lt;/span> &lt;span class="kt">str&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sr_only&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="nb">&amp;#39;static&lt;/span> &lt;span class="kt">str&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">const&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="no">ACCESSIBILITY&lt;/span>: &lt;span class="nc">Accessibility&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Accessibility&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">not_sr_only&lt;/span>: &lt;span class="s">&amp;#34;not-sr-only&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sr_only&lt;/span>: &lt;span class="s">&amp;#34;sr-only&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="cp">#[derive(Clone, Copy)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">struct&lt;/span> &lt;span class="nc">Sizing&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">const&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>: &lt;span class="nc">C&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">acc&lt;/span>: &lt;span class="nc">ACCESSIBILITY&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">siz&lt;/span>: &lt;span class="nc">SIZING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then you can use the generated code like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">use&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">gen&lt;/span>::&lt;span class="p">{&lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">M&lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">class&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[[&lt;/span>&lt;span class="n">M&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">lg&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">siz&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">w_6&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;:&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="n">as_str&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">typ&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">text_lg&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34; &amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;aside>
&lt;small>
(Except that&amp;rsquo;s incredibly disgusting, so I made some macros to make this more
ergonomic. But more on that in a second.)
&lt;/small>
&lt;/aside>
&lt;p>If you remember, back at the beginning of this post, I mentioned that the &lt;code>tailwindcss&lt;/code> program
scans your code to figure out which class names you are using, and then generates a CSS file with
only those classes. But to turn class names like &amp;ldquo;w-3/6&amp;rdquo;, &amp;ldquo;h-0.5&amp;rdquo;, or &amp;ldquo;text-lg&amp;rdquo; into valid Rust
identifiers, I had to transform them a bit. &lt;strong>This means that &lt;code>tailwindcss&lt;/code> will no longer recognize
what classes you&amp;rsquo;re using!&lt;/strong>&lt;/p>
&lt;p>Fortunately, Tailwind allows you to provide a custom &amp;ldquo;extractor&amp;rdquo; to find class names, on a per-file
extension basis. So you need to modify your &lt;code>tailwind.config.js&lt;/code> file:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">content&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">files&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;index.html&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;**/*.rs&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// You do need to copy this big blog of code in, unfortunately.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">extract&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">rs&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">content&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">rs_to_tw&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">rs&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">rs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">startsWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;two_&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">rs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">rs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;two_&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;2&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">rs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">replaceAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;_of_&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;/&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">replaceAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;_p_&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;.&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">replaceAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;_&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;-&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">classes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">class_re&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="sr">/C\.[^ ]+\.([^\. ]+)\b/g&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">mod_re&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="sr">/(?:M\.([^\. ]+)\s*,\s*)+C\.[^ ]+\.([^\. ]+)\b/g&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">matches&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[...&lt;/span>&lt;span class="nx">content&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matchAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">mod_re&lt;/span>&lt;span class="p">)];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">classes&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">map&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">m&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">pieces&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">slice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">pieces&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">map&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">p&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">rs_to_tw&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">p&lt;/span>&lt;span class="p">)).&lt;/span>&lt;span class="nx">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;:&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">classes&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...[...&lt;/span>&lt;span class="nx">content&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matchAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">class_re&lt;/span>&lt;span class="p">)].&lt;/span>&lt;span class="nx">map&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">m&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">rs_to_tw&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">m&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">classes&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>What the custom extractor does is find places in the Rust code that use modifiers or class names,
then it transforms the names from Rust identifiers back to the Tailwind CSS names.&lt;/p>
&lt;p>And with that in place, you now have compile-time checked Tailwind CSS class names, and a workflow
that uses the &lt;code>tailwindcss&lt;/code> tool without requiring &lt;code>node&lt;/code>, &lt;code>npm&lt;/code>, or &lt;code>yarn&lt;/code>.&lt;/p>
&lt;p>You might be tempted to add the &lt;code>tailwindcss-to-rust&lt;/code> invocation to your &lt;code>Trunk.toml&lt;/code> file (or other
bundler tool). But in many cases, this won&amp;rsquo;t be necessary. For most projects, you will run the
generator very rarely, possibly running it once only. The only things that require a re-run are:&lt;/p>
&lt;ol>
&lt;li>You add/remove plugins from your &lt;code>tailwind.config.js&lt;/code>.&lt;/li>
&lt;li>You make changes to your &lt;code>tailwind.config.js&lt;/code> that change the names of custom CSS classes you
have configured.&lt;/li>
&lt;/ol>
&lt;p>So unless you have a config that generates custom names, you will rarely need to regenerate your CSS
file. If you &lt;em>do&lt;/em> have custom config, then it may make sense to have Trunk run
&lt;code>tailwindcss-to-rust&lt;/code>.&lt;/p>
&lt;h2 id="the-ergonomic-macros">The Ergonomic Macros&lt;/h2>
&lt;p>The example I gave of using the generated structs earlier was this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">use&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">gen&lt;/span>::&lt;span class="p">{&lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">M&lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">class&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[[&lt;/span>&lt;span class="n">M&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">lg&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">siz&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">w_6&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;:&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="n">as_str&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">typ&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">text_lg&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34; &amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I said this was gross, and there are a couple reasons I think so. First, I hate having to manually
join modifiers with a colon, and then the overall class list with the space. Second, because the
first join with the modifier produces a &lt;code>String&lt;/code>, you have to convert it to a &lt;code>&amp;amp;str&lt;/code> to join it with
the static &lt;code>&amp;amp;str&lt;/code> in &lt;code>C.typ.text_lg&lt;/code>. You could also write &lt;code>C.typ.text_lg.to_string()&lt;/code> and drop the
earlier &lt;code>.as_str()&lt;/code>. But yuck either way.&lt;/p>
&lt;p>You&amp;rsquo;ll be using these modifiers and classes a &lt;em>lot&lt;/em>, so having to constantly repeat these &lt;code>join&lt;/code>
calls is horrible. To make using this generated code &lt;em>not&lt;/em> horrible, I wrote
&lt;a href="https://lib.rs/crates/tailwindcss-to-rust-macros">a crate with helper macros called &lt;code>tailwindcss-to-rust-macros&lt;/code>&lt;/a>.
Much of this crate&amp;rsquo;s content is a slightly tweaked version of code copied from
&lt;a href="https://seed-rs.org/">the Seed framework&lt;/a> codebase, adjusted to make it more generic.&lt;/p>
&lt;p>Using the macros looks like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">class&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="fm">C!&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="fm">M!&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">M&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">lg&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">siz&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">w_6&lt;/span>&lt;span class="p">],&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">typ&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">text_lg&lt;/span>&lt;span class="p">];&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Yay, no &lt;code>join&lt;/code> calls! The &amp;ldquo;arguments&amp;rdquo; to these macros can be any of these types:&lt;/p>
&lt;ul>
&lt;li>&lt;code>&amp;amp;str&lt;/code>&lt;/li>
&lt;li>&lt;code>String&lt;/code>&lt;/li>
&lt;li>&lt;code>&amp;amp;String&lt;/code>&lt;/li>
&lt;li>&lt;code>Option&amp;lt;T&amp;gt;&lt;/code> and &lt;code>&amp;amp;Option&amp;lt;T&amp;gt;&lt;/code> where &lt;code>T&lt;/code> is any of the above.&lt;/li>
&lt;li>&lt;code>Vec&amp;lt;T&amp;gt;&lt;/code>, &lt;code>&amp;amp;Vec&amp;lt;T&amp;gt;&lt;/code>, and &lt;code>&amp;amp;[T]&lt;/code> where &lt;code>T&lt;/code> is any of the above.&lt;/li>
&lt;/ul>
&lt;p>There&amp;rsquo;s also a &lt;code>DC![...]&lt;/code> macro for use with Dioxus inside its &lt;code>rsx!&lt;/code> macro.&lt;/p>
&lt;p>The big downside of using macros is that you won&amp;rsquo;t get any auto-completion help from your IDE inside
the macros, at least for now&lt;sup id="fnref:9">&lt;a href="#fn:9" class="footnote-ref" role="doc-noteref">9&lt;/a>&lt;/sup>. This is a bit ironic since one of my main motivations for this
tool was to make something that worked better with auto-completion. But there are some tricks. You
can write this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">class&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[[&lt;/span>&lt;span class="n">M&lt;/span>&lt;span class="o">....&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">siz&lt;/span>&lt;span class="o">....&lt;/span>&lt;span class="p">],&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">typ&lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Where the &lt;code>...&lt;/code> is where your IDE will kick in and provide auto-completion. Then you can transform
that into the equivalent macros. I bet you could even write an editor plugin to do this for you, but
I haven&amp;rsquo;t done this yet.&lt;/p>
&lt;p>If you hate macros, you could just write some helper functions:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fn&lt;/span> &lt;span class="nf">m&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">names&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="kt">str&lt;/span>&lt;span class="p">])&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="nb">String&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">names&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;:&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">fn&lt;/span> &lt;span class="nf">c&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">classes&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="kt">str&lt;/span>&lt;span class="p">])&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="nb">String&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">classes&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34; &amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">class&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">c&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">m&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">M&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">lg&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">siz&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">w_6&lt;/span>&lt;span class="p">]),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">typ&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">text_lg&lt;/span>&lt;span class="p">]);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This isn&amp;rsquo;t entirely terrible, but that sure is a lot of references to read. And they don&amp;rsquo;t handle
all the &lt;code>Option&lt;/code> and &lt;code>Vec&lt;/code>/slice combinations that the macros handle.&lt;/p>
&lt;h2 id="a-future-feature">A Future Feature?&lt;/h2>
&lt;p>One person who looked at this tool commented that they didn&amp;rsquo;t like the name transformations I used
and would prefer to just use the original Tailwind names in code. I was thinking about how this
might work and I &lt;em>think&lt;/em> you could use these names with a procedural macro. So you could write this
&amp;hellip;&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">class&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="fm">C!&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">&amp;#34;hidden&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;lg:visible&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;w-6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;text-lg&amp;#34;&lt;/span>&lt;span class="p">];&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&amp;hellip; and it would produce code something like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">lay&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">hidden&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">M&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">lg&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">lay&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">visible&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">siz&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">w_6&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">typ&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">text_lg&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">class&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">&amp;#34;hidden&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;lg:visible&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;w-6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;text-lg&amp;#34;&lt;/span>&lt;span class="p">];&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>But there are a some wrinkles. First, it&amp;rsquo;s not clear how to go from a class name to its &lt;em>group&lt;/em> at
compile time. How does the macro know that &amp;ldquo;hidden&amp;rdquo; belongs to &lt;code>C.lay&lt;/code>? This might require producing
a single struct with all the classes so the generated code could just reference &lt;code>C.hidden&lt;/code>. Or maybe
it could generate a bunch of structs split up by the first letter of the class name if one big
struct causes editor issues.&lt;/p>
&lt;p>Second, I suspect the compilation errors from typos will be kind of horrible, since they&amp;rsquo;ll end up
referring to things like &lt;code>C.lay.hiddden&lt;/code> that simply don&amp;rsquo;t exist in the code you wrote.&lt;/p>
&lt;p>But if someone wants this, please
&lt;a href="https://github.com/houseabsolute/tailwindcss-to-rust/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc">make an issue in the repo&lt;/a>
and we can discuss it.&lt;/p>
&lt;h2 id="putting-it-all-together">Putting It All Together&lt;/h2>
&lt;p>You&amp;rsquo;ll probably want a module in your code that wraps up the generated code and macros together into
a convenient set of exports.
&lt;a href="https://docs.rs/tailwindcss-to-rust-macros/latest/tailwindcss_to_rust_macros/">The macros documentation&lt;/a>
shows you how to do that.&lt;/p>
&lt;p>There are a lot of moving parts here, so here&amp;rsquo;s the summary:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>Follow
&lt;a href="https://lib.rs/crates/tailwindcss-to-rust">the instructions for installing and running the &lt;code>tailwindcss-to-rust&lt;/code> tool&lt;/a>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Create the module as described in
&lt;a href="https://docs.rs/tailwindcss-to-rust-macros/latest/tailwindcss_to_rust_macros/">the docs for the macros&lt;/a>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Import the module and use it:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">use&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">css&lt;/span>::&lt;span class="o">*&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">fn&lt;/span> &lt;span class="nf">some_func&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">class&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="fm">C!&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">spc&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">p_2&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">typ&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">text_white&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">M!&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">M&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">hover&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">C&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">typ&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">text_blue&lt;/span>&lt;span class="p">],&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">];&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ol>
&lt;p>And that&amp;rsquo;s how you can have compile-time checking for your Tailwind class names. Of course, in doing
all of this I&amp;rsquo;ve probably learned more about the Tailwind class names than I ever knew before, so
I&amp;rsquo;ll never typo a class name again. Hah!&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>See my &lt;a href="https://blog.urth.org/2022/02/14/frontend-rust-without-node/">&amp;ldquo;Frontend Rust Without Node&amp;rdquo; post&lt;/a> for a lot more details on what the &lt;code>tailwindcss&lt;/code>
tool is and how it&amp;rsquo;s used.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Which you can automate with &lt;a href="https://trunkrs.dev/">Trunk&lt;/a>. See my &lt;a href="https://blog.urth.org/2022/02/14/frontend-rust-without-node/">&amp;ldquo;Frontend Rust Without Node&amp;rdquo;
post&lt;/a> for example code.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>&lt;a href="https://trunkrs.dev/">Trunk&lt;/a> should make sure this happens by appending a content hash to the
CSS file to ensure your browser doesn&amp;rsquo;t use an old cached version.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>The template also uses PostCSS to generate the &amp;ldquo;compiled&amp;rdquo; Tailwind CSS file, which is a way to
use Tailwind without needing to run &lt;code>tailwindcss&lt;/code>.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>I&amp;rsquo;m using &lt;a href="https://www.gnu.org/software/emacs/">Emacs&lt;/a> (a great OS with excellent editing
built-in) along with the fabulous &lt;a href="https://emacs-lsp.github.io/lsp-mode/">LSP mode&lt;/a> to give me
the full IDE experience. As an aside, I only started using LSP mode a few years ago, and it&amp;rsquo;s
been a huge game-changer when writing code in languages with a good LSP server, mostly Go and
Rust in my case.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:6">
&lt;p>I &lt;em>could&lt;/em> have written this in Rust, but for me this sort of thing is much, much quicker to whip
up in Perl, especially using some great libraries off CPAN, notably
&lt;a href="https://metacpan.org/pod/LWP::Simple">&lt;code>LWP::Simple&lt;/code>&lt;/a> and
&lt;a href="https://metacpan.org/pod/Mojo::DOM">&lt;code>Mojo::DOM&lt;/code>&lt;/a>.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:7">
&lt;p>I put &amp;ldquo;parses&amp;rdquo; in quotes because all it does is use a regex to match names like &amp;ldquo;.foo&amp;rdquo;. I tried
using some CSS parsing crates but they were all enormously complex, and just getting a list of
all the classes in a file was ridiculously hard. But then I remembered I&amp;rsquo;m an old-school Perl
hacker and that regexes are always the best worst solution to any problem.&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:8">
&lt;p>The default is &lt;code>pub(crate)&lt;/code> but you can make it &lt;code>pub&lt;/code> with the &lt;code>--visibility&lt;/code> flag.&amp;#160;&lt;a href="#fnref:8" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:9">
&lt;p>See
&lt;a href="https://rust-analyzer.github.io/blog/2021/11/21/ides-and-macros.html">the &amp;ldquo;IDEs and Macros&amp;rdquo; post&lt;/a>
on the rust-analyzer blog for why.&amp;#160;&lt;a href="#fnref:9" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Frontend Rust Without Node</title><link>https://blog.urth.org/2022/02/14/frontend-rust-without-node/</link><pubDate>Mon, 14 Feb 2022 11:40:55 -0600</pubDate><guid>https://blog.urth.org/2022/02/14/frontend-rust-without-node/</guid><description>&lt;p>When I started &lt;a href="https://blog.urth.org/2022/02/08/my-rust-frontend-experiences/">my frontend Rust project&lt;/a>, I used a &lt;a href="https://seed-rs.org/">Seed&lt;/a> project
&lt;a href="https://github.com/seed-rs/seed-quickstart-webpack">starter template&lt;/a> that included
&lt;a href="https://tailwindcss.com/">Tailwind&lt;/a>, &lt;a href="https://webpack.js.org/">webpack&lt;/a>, some more JS frontend dev
stuff, and some &lt;a href="https://www.typescriptlang.org/">TypeScript&lt;/a> glue code that launches the app.&lt;/p>
&lt;p>This was a &lt;em>lot&lt;/em> of stuff. Stuff for me to install. Stuff for me to run. Stuff for me to (not)
understand. So. Much. Stuff!&lt;/p>
&lt;p>To be clear, I don&amp;rsquo;t want to dump on the author of this template. For one thing, it was created in
2019, and Rust frontend has advanced a &lt;em>lot&lt;/em> since then&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>. And the template did what it says. It
&lt;em>was&lt;/em> a quick start. I just didn&amp;rsquo;t understand how most of it worked. And it was a bit slow to run on
code changes. And making changes to it was frustrating because of all the stuff I didn&amp;rsquo;t understand.&lt;/p>
&lt;p>So when I went to try &lt;a href="https://dioxuslabs.com/">Dioxus&lt;/a>, I wanted to see if I could avoid using any
Node technologies, especially a bundler like webpack, and doubly especially avoiding any JS/TS glue
code for the app.&lt;/p>
&lt;p>Can I avoid that? Well, let&amp;rsquo;s figure that out.&lt;/p>
&lt;h2 id="what-does-webpack-do">What Does Webpack Do?&lt;/h2>
&lt;p>It&amp;rsquo;s called a &amp;ldquo;bundler&amp;rdquo;, which is pretty clear. It takes all your stuff and bundles it into a thing
you can run from a local dev server or distribute to production. That stuff includes:&lt;/p>
&lt;ul>
&lt;li>Your JS/TS&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> application code.&lt;/li>
&lt;li>At least one HTML file, which you need to kick off the application in the browser.&lt;/li>
&lt;li>Your CSS, which may need to be generated from
&lt;a href="https://sass-lang.com/documentation/syntax">SCSS or SASS&lt;/a>.&lt;/li>
&lt;li>Images.&lt;/li>
&lt;li>Fonts.&lt;/li>
&lt;li>Anything else.&lt;/li>
&lt;/ul>
&lt;p>The output of Webpack is an &lt;code>index.html&lt;/code> file that has been processed to load your compiled CSS&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>,
your compiled JS&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>, and maybe other stuff. It will also place the compiled CSS, images, fonts,
etc. in a single directory tree, usually in a top-level folder named &lt;code>dist&lt;/code>. Then you can start a
dev server to serve this tree or ship it off to production (by making a tarball, a Docker image,
etc.).&lt;/p>
&lt;p>When working on a frontend application in Rust, you still need to do some of these tasks. You need
to compile your application from Rust to WASM. You need an &lt;code>index.html&lt;/code> to load that application.
You&amp;rsquo;ll probably want that &lt;code>index.html&lt;/code> to load a CSS file. You might have fonts or images you need
to distribute. And you &lt;em>can&lt;/em> do all of that with webpack.&lt;/p>
&lt;p>But you don&amp;rsquo;t have to!&lt;/p>
&lt;h2 id="just-use-trunkhttpstrunkrsdev">Just Use &lt;a href="https://trunkrs.dev/">Trunk&lt;/a>&lt;/h2>
&lt;p>Trunk is to Rust WASM web apps what Webpack is to JS web apps. It will compile your Rust code to
WASM, process SASS or SCSS files, minify things, copy images, etc.&lt;/p>
&lt;p>Trunk integrates with &lt;a href="https://rustwasm.github.io/docs/wasm-bindgen/">the wasm-bindgen tool&lt;/a>, which
is the CLI tool that turns Rust into WASM. This tool also generates some polyfill code to implement
features not yet implemented in all browsers.&lt;/p>
&lt;p>The &lt;a href="https://rustwasm.github.io/docs/wasm-bindgen/">&lt;code>wasm-bindgen&lt;/code>&lt;/a> crate also provides
&lt;a href="https://docs.rs/wasm-bindgen/latest/wasm_bindgen/">an API for communicating with JS code from Rust&lt;/a>,
which allows you to integrate with existing JS libraries&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup>. There are a number of libraries that
build on top of this integration, like &lt;a href="https://lib.rs/crates/wasm-logger">&lt;code>wasm-logger&lt;/code>&lt;/a>, which
makes the output from &lt;a href="https://lib.rs/crates/log">&lt;code>log&lt;/code> crate&lt;/a> go the browser&amp;rsquo;s console.&lt;/p>
&lt;p>Here&amp;rsquo;s a very minimal &lt;code>index.html&lt;/code> that will make Trunk compile your Rust app:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">html&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;main&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">html&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Wait, what? There&amp;rsquo;s nothing in there! If you don&amp;rsquo;t point it at any code in your HTML, then Trunk
will automatically compile the crate containing your &lt;code>index.html&lt;/code> and turn it into a WASM
application that your index.html loads.&lt;/p>
&lt;p>If you want some CSS you can add this to the &lt;code>&amp;lt;head&amp;gt;&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">link&lt;/span> &lt;span class="na">data-trunk&lt;/span> &lt;span class="na">rel&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;css&amp;#34;&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;/css/my-compiled.css&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If you use &lt;code>rel=&amp;quot;scss&amp;quot;&lt;/code> or &lt;code>rel=&amp;quot;sass&amp;quot;&lt;/code> then Trunk will compiled that file into a CSS file. Trunk
also hashes the file and puts that hash in the path to ensure that browsers reload the CSS whenever
the CSS source changes.&lt;/p>
&lt;p>Other file types all use the same &lt;code>&amp;lt;link rel=&amp;quot;$type&amp;quot; href=&amp;quot;/path/to/file.$type&amp;quot;&amp;gt;&lt;/code> pattern. Trunk
supports icons (for your favicon), images, and it can copy files and directories wholesale for
images, fonts, etc. At the present, it doesn&amp;rsquo;t support any sort of fancy processing of those files
natively, and it doesn&amp;rsquo;t do hashing of them (yet).&lt;/p>
&lt;p>However, you can use your own hooks to make it do other stuff by running arbitrary programs. And
this is how I&amp;rsquo;ve been able to avoid using webpack and I&amp;rsquo;m able to not run node at all for my web
app.&lt;/p>
&lt;p>Ok, I lied, I do run node.&lt;/p>
&lt;h2 id="tailwindcss">Tailwindcss&lt;/h2>
&lt;p>I&amp;rsquo;m using the standalone &lt;code>tailwindcss&lt;/code> executable, which effectively bundles &lt;code>node&lt;/code> plus some JS
code into a single executable. You can download this from
&lt;a href="https://github.com/tailwindlabs/tailwindcss/releases">the tailwindcss GitHub project&amp;rsquo;s releases page&lt;/a>.&lt;/p>
&lt;p>I have an &amp;ldquo;input CSS file&amp;rdquo; for tailwind that looks like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="cl">&lt;span class="p">@&lt;/span>&lt;span class="k">tailwind&lt;/span> &lt;span class="nt">base&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">@&lt;/span>&lt;span class="k">tailwind&lt;/span> &lt;span class="nt">components&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">@&lt;/span>&lt;span class="k">tailwind&lt;/span> &lt;span class="nt">utilities&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I also have a &lt;code>tailwind.config.js&lt;/code> file that looks like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">content&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;index.html&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;**/*.rs&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">theme&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">extend&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">plugins&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;@tailwindcss/forms&amp;#34;&lt;/span>&lt;span class="p">)],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then I have this bit of configuration in my &lt;code>Trunk.toml&lt;/code> file:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[[&lt;/span>&lt;span class="nx">hooks&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">stage&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;build&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># I&amp;#39;m not sure why we can&amp;#39;t just invoke tailwindcss directly, but that doesn&amp;#39;t&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># seem to work for some reason.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">command&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;sh&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">command_arguments&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;-c&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;tailwindcss -i css/tailwind.css -o css/tailwind_compiled.css&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Why can&amp;rsquo;t I just run &lt;code>tailwindcss&lt;/code> directly. I don&amp;rsquo;t have a damn clue.&lt;/p>
&lt;p>What exactly does &lt;code>tailwindcss&lt;/code> do? To answer that, it&amp;rsquo;s important to understand the basic design of
Tailwind. Unlike most CSS frameworks, with Tailwind you don&amp;rsquo;t build your own SASS/SCSS/CSS file
using the framework as a base. You don&amp;rsquo;t define new classes based on Tailwind classes.&lt;/p>
&lt;p>Instead, Tailwind provides hundreds of small utility classes like &lt;code>mr-8&lt;/code> (right margin size 8),
&lt;code>flex&lt;/code> (use a flexbox layout for this element), and &lt;code>text-lg&lt;/code> (make this text larger). You use those
classes directly in your code which generates HTML. Here&amp;rsquo;s an example using JSX:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-jsx" data-lang="jsx">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">App&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">h1&lt;/span> &lt;span class="na">className&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;text-3xl font-bold underline&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="nx">Hello&lt;/span> &lt;span class="nx">world&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This example uses three Tailwind classes, &lt;code>text-3xl&lt;/code>, &lt;code>font-bold&lt;/code>, and &lt;code>underline&lt;/code>. Your first
reaction may be shock and horror. Mine was! But when I read more about the reasoning behind it I
realized that this actually works very nicely with modern frontend web app practices.&lt;/p>
&lt;p>Nowadays, you don&amp;rsquo;t write your HTML in one set of files and then make it dynamic with separate JS
code. Having lots of HTML was the original reason to use CSS classes. It meant that you could have
many different HTML pages with the same styles easily. Anywhere you embedded a search box you&amp;rsquo;d slap
a &lt;code>search&lt;/code> class on the &lt;code>&amp;lt;div&amp;gt;&lt;/code>.&lt;/p>
&lt;p>In modern apps, your JS (or in my case, Rust) code generates the HTML directly. So your HTML
generation can be factored into functions or methods. And that means that you never need to repeat
the same sets of Tailwind classes across your application. If you need to reuse some particular
piece of layout, you can turn that into a reusable component. Here&amp;rsquo;s one from my music player:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#[inline_props]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">fn&lt;/span> &lt;span class="nf">UserFacingError&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="na">&amp;#39;a&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">cx&lt;/span>: &lt;span class="nc">Scope&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">error&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="na">&amp;#39;a&lt;/span> &lt;span class="nc">crate&lt;/span>::&lt;span class="n">client&lt;/span>::&lt;span class="n">Status&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="nc">Element&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">cx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">render&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="fm">rsx!&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">PageTitle&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;Error&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">},&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">div&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">class&lt;/span>: &lt;span class="s">&amp;#34;flex flex-row flex-wrap justify-center&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;{error.message}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">})&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If I need that set of classes, &lt;code>&amp;quot;flex flex-row flex-wrap justify-center&amp;quot;&lt;/code>, in other components then
I can either make a new component for that &lt;code>&amp;lt;div&amp;gt;&lt;/code> with those classes, &lt;em>or&lt;/em> I can just have a
function that returns those classes:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fn&lt;/span> &lt;span class="nf">center_flex_classes&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="kt">str&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;flex flex-row flex-wrap justify-center&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I&amp;rsquo;m using a real programming language to generate HTML, so I can take advantage of that fact to
avoid repeating my CSS classes all over the place&lt;sup id="fnref:6">&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref">6&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>Finally, to tie it all together, I load the CSS in my &lt;code>index.html:&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">link&lt;/span> &lt;span class="na">data-trunk&lt;/span> &lt;span class="na">rel&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;css&amp;#34;&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;/css/tailwind_compiled.css&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="what-does-tailwindcss-do">What Does &lt;code>tailwindcss&lt;/code> Do?&lt;/h2>
&lt;p>Remember that I&amp;rsquo;m running &lt;code>tailwindcss&lt;/code> via Trunk as part of my build process? Why? If tailwind is
just a bunch of already-defined classes what is that command doing?&lt;/p>
&lt;p>Well, calling it a &amp;ldquo;bunch&amp;rdquo; of classes may be understating things. To see how many, I generated a
file containing &lt;em>all&lt;/em> of the available-by-default CSS classes, with various media-size modifiers and
pseudo-classes for &lt;code>hover&lt;/code> and so on. It came out to around 7MB. That&amp;rsquo;s a big CSS file. Too big.&lt;/p>
&lt;p>So what &lt;code>tailwindcss&lt;/code> does is figure out which classes you&amp;rsquo;re using by looking at your code. Then it
generates a CSS file with just the ones that you need. For my app I currently end up with a file
that&amp;rsquo;s just 19k. That&amp;rsquo;s much better than 7MB!&lt;/p>
&lt;h2 id="tailwind-typos">Tailwind Typos&lt;/h2>
&lt;p>Have I mentioned that Tailwind offers a &lt;em>lot&lt;/em> of CSS classes? Take a look at just
&lt;a href="https://tailwindcss.com/docs/padding">the padding classes&lt;/a>. There are a lot, and the names aren&amp;rsquo;t
all that memorable.&lt;/p>
&lt;p>If you typo a name then &lt;code>tailwindcss&lt;/code> simply ignores it and it&amp;rsquo;s missing from the generated CSS. As
I worked on my app I did this a lot. And it was always confusing. Was my layout wrong because of my
HTML? Was it the CSS classes I chose? Did the CSS generation process not regen the file, so the
class wasn&amp;rsquo;t in the generated CSS? Or did I just typo a class name, so something had &amp;ldquo;px-13&amp;rdquo; as a
class, which doesn&amp;rsquo;t exist.&lt;/p>
&lt;p>It turns out I made a lot of typos. I kept wasting time trying to debug why my newly modified CSS
wasn&amp;rsquo;t applying any styles, only to realize I&amp;rsquo;d made a typo.&lt;/p>
&lt;p>Wouldn&amp;rsquo;t it be nice if I could get the Rust compiler to check my CSS class names? Yes, that would be
nice.&lt;/p>
&lt;p>In fact, the Seed quickstart template I&amp;rsquo;d been using provides exactly that through a
&lt;a href="https://postcss.org/">PostCSS&lt;/a>&lt;sup id="fnref:7">&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref">7&lt;/a>&lt;/sup> plugin that generates Rust code from your Tailwind config.&lt;/p>
&lt;p>Wouldn&amp;rsquo;t it be nice to have something like that for Dioxus? And maybe it could be generic enough for
any framework? And maybe it could be written in Rust?&lt;/p>
&lt;p>Yes, that would be fantastic! And I&amp;rsquo;ll talk about that in my next blog post, covering
&lt;a href="https://lib.rs/crates/tailwindcss-to-rust">my new &lt;code>tailwindcss-to-rust&lt;/code> tool&lt;/a>. I&amp;rsquo;ll also cover the
Dioxus helper crate I wrote that makes it super easy&lt;sup id="fnref:8">&lt;a href="#fn:8" class="footnote-ref" role="doc-noteref">8&lt;/a>&lt;/sup> to use
&lt;a href="https://heroicons.com/">SVG icons from HeroIcons&lt;/a> in your Dioxus project.&lt;/p>
&lt;aside>
&lt;small>
P.S. If you &lt;em>don&amp;rsquo;t&lt;/em> want a framework like Tailwind and you&amp;rsquo;d prefer to use CSS styles directly in
your Rust frontend, but you want compile-time safety, check out the
&lt;a href="https://lib.rs/crates/stylist">stylist crate&lt;/a>. Thanks to @cryptoquick on the Dioxus Discord for
making me aware of this crate.
&lt;/small>
&lt;/aside>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>The first commit was in March, 2019, so it&amp;rsquo;s been about three years.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>TS = &lt;a href="https://www.typescriptlang.org/">TypeScript&lt;/a>. Your app could also be in another language
like &lt;a href="https://elm-lang.org/">Elm&lt;/a> or &lt;a href="https://www.purescript.org/">PureScript&lt;/a>.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>Compiled from SASS or generated by &lt;code>tailwindcss&lt;/code> or, in the case of plain CSS files, just
concatenated together and maybe minified.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>Compiled from ES2015 or TS or &lt;a href="https://reactjs.org/docs/introducing-jsx.html">JSX&lt;/a> or Elm
(probably using &lt;a href="https://babeljs.io/">Babel&lt;/a>) and processed to handle imports, then concatenated
into one file and maybe minified.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>Fortunately, I haven&amp;rsquo;t had to do that yet, because these sorts of cross-language integrations
are often painful in my experience.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:6">
&lt;p>Of course, &amp;ldquo;can&amp;rdquo; is the operative word here. Am I doing this consistently? No, because I&amp;rsquo;m doing
a lot of experimentation so I&amp;rsquo;m okay with some quick cut and paste for now. But if I was
building something that I expected many other people to hack on and maintain, I would (I hope)
exercise more discipline.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:7">
&lt;p>Oh yay, another tool to run and another config file in my project I don&amp;rsquo;t understand.&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:8">
&lt;p>Barely an inconvenience!&amp;#160;&lt;a href="#fnref:8" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>What Do I Want from My Next Job? 2022 Edition</title><link>https://blog.urth.org/2022/02/11/what-do-i-want-from-my-next-job-2022-edition/</link><pubDate>Fri, 11 Feb 2022 09:35:56 -0600</pubDate><guid>https://blog.urth.org/2022/02/11/what-do-i-want-from-my-next-job-2022-edition/</guid><description>&lt;p>The &lt;a href="https://blog.urth.org/2021/10/15/what-do-i-want-from-my-next-job-/">last time I wrote about this&lt;/a> was 8 days after I left my last position, at
&lt;a href="https://www.activestate.com/">ActiveState&lt;/a>. Since writing that post, I&amp;rsquo;ve continued to think about
what I want, and my thinking has evolved.&lt;/p>
&lt;hr>
&lt;p>&lt;strong>TLDR:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Top tier TC&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> &lt;em>or&lt;/em> a 4 day week, and both would be ideal (but unlikely).&lt;/li>
&lt;li>5+ weeks of PTO.&lt;/li>
&lt;li>Yes++ to Rust. No Java or PHP.&lt;/li>
&lt;li>No companies in certain fields like crypto.&lt;/li>
&lt;li>I want to do a significant amount of IC work, even if I&amp;rsquo;m in a management position.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>Reflecting on how I felt last October, I was well and truly burned out. The past few years had been
quite tough for me. In September of 2020, my mother died. Then a few months later, my father moved
to Minneapolis from Florida. We purchased a duplex together. He moved in right away, and my wife and
I moved in a few months later in April of 2022.&lt;/p>
&lt;p>This was a lot to deal with. Soon after, some things had happened at my job that had made me
unhappy. I think if this had been a different time in my life, I would have fought harder to change
these things&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>. But at this particular time, I mostly felt exhausted and discouraged. I didn&amp;rsquo;t
have the energy to tackle the issues that had come up. So instead, I waited until we sold our
previous house, which gave us enough cash in the bank for me to take some time off.&lt;/p>
&lt;p>I&amp;rsquo;m really glad I did that. This break has been fantastic. I&amp;rsquo;ve ended up coding more during this
break than I did at ActiveState. It&amp;rsquo;s amazing how easy it is to write code when you&amp;rsquo;re the only
customer and you can work on whatever you want.&lt;/p>
&lt;p>But I&amp;rsquo;ve also been thinking more about what I want from my next job, and more broadly, what I want
out of life&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>.&lt;/p>
&lt;h2 id="working-less-vs-compensation">Working Less vs Compensation&lt;/h2>
&lt;p>Last time I wrote about this, I said this:&lt;/p>
&lt;blockquote>
&lt;p>&lt;del>My number one criteria for my next job is being able to work 4 days per week (or less)&lt;/del>.&lt;/p>
&lt;/blockquote>
&lt;p>But I think this was mostly my burnout talking. While I do like the idea of working less, what I
realized was that this should be a long-term goal too. That means that I should be willing to work a
bit more now to work less later. Specifically, if I can retire earlier than average, that&amp;rsquo;s a lot
less working.&lt;/p>
&lt;p>So here&amp;rsquo;s my new number one goal:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>I want to work less, but that doesn&amp;rsquo;t have to happen this year or the next.&lt;/strong>&lt;/p>
&lt;/blockquote>
&lt;p>All of which is to say that I think I&amp;rsquo;d be perfectly happy to take a higher compensation 5-day job
right now so I can save a significant amount of money towards earlier retirement. I&amp;rsquo;m not aiming for
the full &lt;a href="https://en.wikipedia.org/wiki/FIRE_movement">FIRE&lt;/a>. I&amp;rsquo;m already too old for that. But I
think it&amp;rsquo;s realistic to aim to retire in my late 50&amp;rsquo;s or early 60&amp;rsquo;s.&lt;/p>
&lt;p>But I&amp;rsquo;d still consider a 4-day week too, with the expectation that the highest TC jobs will probably
require 5 day weeks.&lt;/p>
&lt;h2 id="pto">PTO&lt;/h2>
&lt;p>I also said:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>I also want at least 5 weeks of PTO per year.&lt;/strong>&lt;/p>
&lt;/blockquote>
&lt;p>This still seems pretty reasonable. I&amp;rsquo;m sticking with this one, especially if I&amp;rsquo;m working 5 days a
week.&lt;/p>
&lt;h2 id="languages-and-technology">Languages and Technology&lt;/h2>
&lt;p>&lt;strong>I&amp;rsquo;d still love to work with &lt;a href="https://www.rust-lang.org/">Rust&lt;/a>.&lt;/strong> During the last 5 months I&amp;rsquo;ve
mostly been writing Rust, and I&amp;rsquo;ve greatly enjoyed it. I haven&amp;rsquo;t touched Go since I left
ActiveState, though I plan to refresh myself before I start interviewing in earnest.&lt;/p>
&lt;p>Everything else I said in my last post about this topic still stands true.&lt;/p>
&lt;h2 id="company-size-and-stage">Company Size and Stage&lt;/h2>
&lt;p>I&amp;rsquo;m still open to pretty much anything. If I want to focus more on total compensation, I think I
will by necessity be looking at larger companies as well, and that&amp;rsquo;s fine.&lt;/p>
&lt;h2 id="ic3-or-management">IC&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup> or Management?&lt;/h2>
&lt;p>Same as last time. I&amp;rsquo;m open to either, but if I&amp;rsquo;m managing I&amp;rsquo;d like to be able to code as well.&lt;/p>
&lt;h2 id="companys-product">Company&amp;rsquo;s Product&lt;/h2>
&lt;p>Again, same as last time.&lt;/p>
&lt;ul>
&lt;li>No crypto/blockchain.&lt;/li>
&lt;li>No social networking&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup>.&lt;/li>
&lt;li>No gig economy work for non-professionals.&lt;/li>
&lt;/ul>
&lt;p>I&amp;rsquo;d also add &amp;ldquo;no companies that use animals or animal products&amp;rdquo;, though this is uncommon in the tech
field anyway. I guess I&amp;rsquo;d probably be avoiding many biotech companies because of animal testing, but
given my skill set, I don&amp;rsquo;t think I&amp;rsquo;d be a good fit for that field anyway.&lt;/p>
&lt;h2 id="narrowing-it-down">Narrowing it Down&lt;/h2>
&lt;p>My hard requirements are:&lt;/p>
&lt;ul>
&lt;li>Top tier TC &lt;em>or&lt;/em> a 4 day week, and both would be ideal.&lt;/li>
&lt;li>5+ weeks of PTO.&lt;/li>
&lt;li>Yes to Rust. No Java or PHP.&lt;/li>
&lt;li>No companies in the unacceptable fields I&amp;rsquo;ve mentioned.&lt;/li>
&lt;li>I want to do a significant amount of IC work, even if I&amp;rsquo;m in a management position.&lt;/li>
&lt;/ul>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>TC = total compensation&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Yes, I&amp;rsquo;m being vague. There&amp;rsquo;s no need to air dirty laundry.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>I&amp;rsquo;m 48, which means my life is very likely more than half over. I think it&amp;rsquo;s good to keep that
in mind as I make big decisions.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>IC = individual contributor, aka not management&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>I also included &amp;ldquo;surveillance capitalism&amp;rdquo; in my last post, but it occurs to me I&amp;rsquo;m not 100% sure
what that means. I need to think this one through a bit more clearly. I know I wouldn&amp;rsquo;t work for
Facebook, but would I work for Google? My gut instinct is that they have many products that
seems perfectly fine to work on, like &lt;a href="https://console.cloud.google.com/">GCP&lt;/a>.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>My Rust Frontend Experiences</title><link>https://blog.urth.org/2022/02/08/my-rust-frontend-experiences/</link><pubDate>Tue, 08 Feb 2022 17:11:40 -0600</pubDate><guid>https://blog.urth.org/2022/02/08/my-rust-frontend-experiences/</guid><description>&lt;p>As &lt;a href="https://blog.urth.org/2021/12/22/working-with-musicbrainz-name-data/">I mentioned in a previous post&lt;/a>, I&amp;rsquo;ve been working on a music player for a
while as a fun side project. Though since I&amp;rsquo;ve been jobless this has actually been my primary
project, and I&amp;rsquo;ve probably spent more time coding than I did at work&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.&lt;/p>
&lt;link rel="stylesheet" href="https://blog.urth.org/css/hugo-easy-gallery.css" />
&lt;div class="box">
&lt;figure itemprop="associatedMedia"
itemscope itemtype="http://schema.org/ImageObject" >
&lt;div class="img">
&lt;img itemprop="thumbnail" src="https://blog.urth.org/image/crumb.png" alt="The artists list in my webapp"/>
&lt;/div>
&lt;a href="https://blog.urth.org/image/crumb.png" itemprop="contentUrl">&lt;/a>
&lt;figcaption>
&lt;p>This almost looks like a real app but don&amp;#39;t be fooled. Half the buttons don&amp;#39;t work yet.&lt;/p>
&lt;/figcaption>
&lt;/figure>
&lt;/div>
&lt;p>My goal was always to build a backend that could support multiple frontends, especially a web app
and mobile apps. I decided to work on the web app frontend first. I wanted to build &lt;em>a&lt;/em> frontend
quickly so I could work on the backend and importer and have a way to exercise them.&lt;/p>
&lt;p>Rust can compile down to &lt;a href="https://webassembly.org/">WASM&lt;/a>, which means you can run Rust in the
browser, and there are quite a few Rust frameworks for frontend development. Check out this
&lt;a href="https://blog.logrocket.com/current-state-rust-web-frameworks/">LogRocket blog post for a good list&lt;/a>
that&amp;rsquo;s current as of early 2022&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>. There are a number of options including some that provide an
experience very similar to &lt;a href="https://reactjs.org/">React&lt;/a> or &lt;a href="https://elm-lang.org/">Elm&lt;/a>.&lt;/p>
&lt;p>My initial plan was to use Rust, but after a little investigation I ended up trying Flutter instead.
It promises to support both mobile apps and the web, and there was no Rust frontend framework that
did that. But then some quick experiments with Flutter&amp;rsquo;s web output convinced me not to use it.
Flutter&amp;rsquo;s web renderer works by creating a &lt;code>&amp;lt;canvas&amp;gt;&lt;/code> tag and drawing your entire app inside it.
This means that &lt;em>nothing&lt;/em> in the app works the way you&amp;rsquo;d expect. You can&amp;rsquo;t even select text without
explicitly re-implementing it. So it basically breaks everything the browser does. No. Just no.&lt;/p>
&lt;p>So it was back to Rust. After looking at a few options, I decided to start with
&lt;a href="https://seed-rs.org/">Seed&lt;/a>. It was web only, unfortunately, but it seemed like a good design. I
got something working fairly quickly and it was a good experience. The message-passing based API
meant I had to create a &lt;em>lot&lt;/em> of &lt;code>Msg&lt;/code> enums, basically one per component with any interactive
piece. And then I was constantly calling &lt;code>some_view(foo).map_msg(Msg::SomeViewMsg)&lt;/code> because of the
message types. If this makes no sense, just trust me that it&amp;rsquo;s a bit annoying, but not a
dealbreaker.&lt;/p>
&lt;p>But then a month ago, Jonathan Kelley announced &lt;a href="https://dioxuslabs.com/">Dioxus&lt;/a>, which supports
the web as well as mobile&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> and desktop apps. This was exactly what I&amp;rsquo;d wanted!&lt;/p>
&lt;p>So I decided to give it a try. If it worked well for the web app, I&amp;rsquo;d be way ahead on desktop and
mobile versions too. I&amp;rsquo;ve been working with it for a few months and I quite like it. It&amp;rsquo;s very
React-like, though since my last use of React was in 2017, the whole &amp;ldquo;hooks&amp;rdquo; thing is new to me. The
&lt;a href="https://discord.gg/XgGxMSkvUM">Dioxus Discord community&lt;/a> has been great, and the author has been
&lt;em>incredibly&lt;/em> responsive to questions and bug reports. Often he fixes things within minutes or hours
of my report, and he&amp;rsquo;s also been very helpful when I&amp;rsquo;m confused about how to do something (which
happens a lot).&lt;/p>
&lt;p>I even wrote a first draft of a (web-only for now) router, which was a fun learning exercise for me.
The Dioxus author merged it pretty quickly&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>, so it&amp;rsquo;s there for anyone else to try out too.&lt;/p>
&lt;p>I&amp;rsquo;ve also released a few more Rust crates related to frontend work, but I&amp;rsquo;ll go into those in a
future blog post. I&amp;rsquo;ll also talk a bit about how I set this up the development environment, and how
I&amp;rsquo;ve so far managed to avoid needing any Node tools like &lt;a href="https://webpack.js.org/">webpack&lt;/a>.&lt;/p>
&lt;p>I&amp;rsquo;ll be looking for a new position soon&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup>, but until then I&amp;rsquo;ll keep working on this app. It&amp;rsquo;s been
a great learning experience, and a lot of fun too.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Hey, I was in management. I wasn&amp;rsquo;t slacking.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Which means it will be out of date in a few months? Weeks?&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>Just iOS for now though.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>So maybe he doesn&amp;rsquo;t have the best judgement all the time.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>My current plan is to start my search in March, but if you have a great Rust job for me please
&lt;a href="mailto:autarch@urth.org">let me know&lt;/a>.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>My PDF Resume Script</title><link>https://blog.urth.org/2022/01/08/my-pdf-resume-script/</link><pubDate>Sat, 08 Jan 2022 10:26:12 -0600</pubDate><guid>https://blog.urth.org/2022/01/08/my-pdf-resume-script/</guid><description>&lt;p>&lt;strong>Edit 2022-01-19: I&amp;rsquo;ve since
&lt;a href="https://github.com/autarch/houseabsolute.com/tree/master/resume">moved the script to a public repo&lt;/a>.
Also, I&amp;rsquo;ve reformatted my resume to make it a bit shorter, so here&amp;rsquo;s a
&lt;a href="https://drive.google.com/file/d/1Q3bb_3cLZErAYxgFgDaPBNLaCT-TfWbi/view?usp=sharing">more recent PDF version&lt;/a>.&lt;/strong>&lt;/p>
&lt;p>I only want to have &lt;a href="https://houseabsolute.com/resume/">one canonical resume&lt;/a>, and I want to keep it
on my personal website. That makes it trivial to update, and anyone with the link can see the latest
version at all times.&lt;/p>
&lt;p>But unfortunately very few job application systems will accept a link to a resume. Most want a
document of some kind. I didn&amp;rsquo;t want to maintain a second copy as a Google Doc or something like
that, so I wrote
&lt;a href="https://gist.github.com/autarch/4b3d04bb08639eb0413c7bde8d9b65ce">a script to transform the web version to a PDF&lt;/a>.
&lt;del>Here&amp;rsquo;s a copy of the PDF version&lt;/del>.&lt;/p>
&lt;p>When I first looked into doing this I was afraid it would be a lot of work. Fortunately, it turned
out to be super easy&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>. It&amp;rsquo;s in Perl, of course. This script is true glue code, and Perl made this
work trivial.&lt;/p>
&lt;p>All it does is grab the raw HTML from the page, munge it a bit, and then pass it through
&lt;a href="https://pandoc.org/">Pandoc&lt;/a>. Pandoc is great. It uses LaTex as the intermediate format to generate
the PDF, so it ends up looking very professional (IMO) with no work on my part!&lt;/p>
&lt;p>I wrote the first version back in 2016 when I knew I was looking for a new position. I&amp;rsquo;ve made a few
tweaks to it over the years as the web version has changed, but not many.&lt;/p>
&lt;p>So in summary, yay Perl, yay Pandoc.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Barely an inconvenience.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Working with MusicBrainz Name Data</title><link>https://blog.urth.org/2021/12/22/working-with-musicbrainz-name-data/</link><pubDate>Wed, 22 Dec 2021 23:18:15 -0600</pubDate><guid>https://blog.urth.org/2021/12/22/working-with-musicbrainz-name-data/</guid><description>&lt;p>Since leaving my job last October I&amp;rsquo;ve been working on various personal projects for fun and
learning. One of those is a music player&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> written entirely in &lt;a href="https://www.rust-lang.org/">Rust&lt;/a>,
including a web-based frontend using &lt;a href="https://seed-rs.org/">Seed&lt;/a>, which is an Elm-like frontend
framework in Rust. I&amp;rsquo;d like to write some other posts about that as well, but today let&amp;rsquo;s talk about
artist names, specifically how these are represented in the &lt;a href="https://musicbrainz.org/">MusicBrainz&lt;/a>
data.&lt;/p>
&lt;p>I have a &lt;em>lot&lt;/em> of Japanese music&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>, and one of the main reasons I don&amp;rsquo;t like any of the music
players I&amp;rsquo;ve tried so far&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> is their handling of non-Latin script names and titles. More
specifically, most players only allow one name (plus a sortable name if you&amp;rsquo;re lucky). I find this
annoying because what I&amp;rsquo;d really like to see in many cases is a Latin transcription or English
translation instead of the original. For reference, a transcription is a phonetic representation of
a name.&lt;/p>
&lt;p>I prefer to see a transcribed name like &lt;a href="https://www.ryokushaka.com/">&amp;ldquo;Ryokushaka&amp;rdquo;&lt;/a> instead of
&amp;ldquo;緑黄色社会&amp;rdquo;. I also prefer the transcription over the translation, &amp;ldquo;Green Yellow Society&amp;rdquo;. But it&amp;rsquo;s
nice to be able to &lt;em>see&lt;/em> the Japanese name as well, especially if I&amp;rsquo;m trying to search the web for
information about the artist or search YouTube for videos.&lt;/p>
&lt;p>So what I really want is a tool that can handle multiple canonical values for each name or title,
specifically the name according to the artist as well as optional transcribed and translated names
for non-Latin names. And each of those canonical names also needs a sortable version (at least for
artists), so &amp;ldquo;The Beatles&amp;rdquo; sorts as &amp;ldquo;B&amp;rdquo;, not &amp;ldquo;T&amp;rdquo;.&lt;/p>
&lt;p>In order to accomplish this, I need to get more data than my MP3 files have. The best data source
I&amp;rsquo;ve found is the MusicBrainz project, which is a fantastic open source/open data project for
collecting information about all recorded music, or in their words, &amp;ldquo;MusicBrainz is an open music
encyclopedia that collects music metadata and makes it available to the public.&amp;rdquo; It&amp;rsquo;s a lot like
Wikipedia in that anyone can make an account and contribute edits. It&amp;rsquo;s also a lot like Wikipedia in
that the quality and correctness of entries varies wildly.&lt;/p>
&lt;p>MusicBrainz (MB) allows for artists, releases (albums), and tracks to have many different
names/titles. I will focus on artists for now, since artists have the most complex name data. An
artist has a primary name, an optional sortable name, and zero or more aliases. Each alias has a
name, an optional sortable name, an optional locale, a boolean indicating whether its the primary
alias for a locale, and a type. Artists may also appear with different names in artist credits,
either of their own albums or others, like compilation albums or as guest artists on someone else&amp;rsquo;s
album.&lt;/p>
&lt;p>The possible alias types are &amp;ldquo;Artist name&amp;rdquo;, &amp;ldquo;Legal name&amp;rdquo;, and &amp;ldquo;Search hint&amp;rdquo;. An &amp;ldquo;Artist name&amp;rdquo; is
another version of the name they perform under. A &amp;ldquo;Legal name&amp;rdquo; alias is used to record the name for
people who perform under an alias, like Madonna or Lady Gaga. I&amp;rsquo;m just filtering these out, since
they&amp;rsquo;re not interesting for my purposes. A &amp;ldquo;Search hint&amp;rdquo; can be a common misspelling of their name,
a fan nickname, or anything else that helps the search engine be smarter.&lt;/p>
&lt;p>To make things a little more complex, MB has a policy of having the primary sortable name &lt;em>always&lt;/em>
be in Latin script, even if the primary name is not.&lt;/p>
&lt;p>Let&amp;rsquo;s use &lt;a href="http://www.sexystones.com/">Kenichi Asai&lt;/a>&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup> as an example. For reference, his personal
name (aka &amp;ldquo;first name&amp;rdquo;) is &amp;ldquo;Kenichi&amp;rdquo; and his family name (aka &amp;ldquo;last name&amp;rdquo;) is &amp;ldquo;Asai&amp;rdquo;. In English
we&amp;rsquo;d typically write &amp;ldquo;Kenichi Asai&amp;rdquo;, but you might also see &amp;ldquo;Asai Kenichi&amp;rdquo;, because Japanese usage
always put the family name first. Sometimes you&amp;rsquo;ll also see &amp;ldquo;ASAI Kenichi&amp;rdquo; to denote that &amp;ldquo;Asai&amp;rdquo; is
the family name.&lt;/p>
&lt;p>Here are all of his names in the MB data:&lt;/p>
&lt;!-- prettier-ignore-start -->
&lt;!-- prettier absolutely butchers this table -->
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;strong>Name&lt;/strong>&lt;/th>
&lt;th>&lt;strong>Sortable name&lt;/strong>&lt;/th>
&lt;th>&lt;strong>Locale&lt;/strong>&lt;/th>
&lt;th>&lt;strong>Alias type&lt;/strong>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Primary name&lt;/strong>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>浅井健一&lt;/td>
&lt;td>Asai, Kenichi&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>As I noted, MB has a policy of always using a Latin script sortable name for primary names.&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Aliases&lt;/strong>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Kenichi Asai&lt;/td>
&lt;td>Asai, Kenichi&lt;/td>
&lt;td>en (primary)&lt;/td>
&lt;td>Artist name&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>浅井健一&lt;/td>
&lt;td>あさいけんいち&lt;/td>
&lt;td>ja (primary)&lt;/td>
&lt;td>Artist name&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>The sortable name here is the Kanji (Chinese characters) written entirely in Hiragana&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup> in order to make the pronunciation clear.&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>浅井 健一&lt;/td>
&lt;td>あさい けんいち&lt;/td>
&lt;td>ja&lt;/td>
&lt;td>Artist name&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>This one differs from the previous one by having a space between the family and personal names, which is not how Japanese is typically written.&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Asai Ken&amp;rsquo;ichi&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>Search hint&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Benzie&lt;/td>
&lt;td>ベンジー&lt;/td>
&lt;td>&lt;/td>
&lt;td>Artist name&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Artist credits&lt;/strong>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Asai Kenichi&lt;/td>
&lt;td>&lt;/td>
&lt;td>Kenichi Asai&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;!-- prettier-ignore-end -->
&lt;p>Wow, that&amp;rsquo;s a lot names! But we can see that there&amp;rsquo;s a number of duplicates.&lt;/p>
&lt;p>So the question is how to take that list and boil it down to the following elements:&lt;/p>
&lt;ul>
&lt;li>The &amp;ldquo;real&amp;rdquo; name, by which I mean the name the artist typically uses in their home locale.&lt;/li>
&lt;li>A sortable version of that real name if it differs from the real name.&lt;/li>
&lt;li>A transcribed name if the real name is not in Latin script.&lt;/li>
&lt;li>A translated name if the real name is not in Latin script &lt;em>and&lt;/em> the real name is not a personal
name. In other words, it doesn&amp;rsquo;t make sense to &amp;ldquo;translate&amp;rdquo; 浅井健一 (Kenichi Asai), which is a
personal name. But it &lt;em>does&lt;/em> make sense to translate a band name, for example
translating 緑黄色社会 into &amp;ldquo;Green Yellow Society&amp;rdquo;.&lt;/li>
&lt;/ul>
&lt;p>The answer, of course, is to use a whole bunch of heuristics, because this is impossible to get 100%
right, especially since the source data can be incorrect and incomplete. There&amp;rsquo;s not a lot of
consistency in how people end up inputting non-Latin names, and I&amp;rsquo;m pretty sure the artist credit of
&amp;ldquo;Asai Kenichi&amp;rdquo; is just wrong and the correct credit is either &amp;ldquo;浅井健一&amp;rdquo; or &amp;ldquo;Kenichi Asai&amp;rdquo;.&lt;/p>
&lt;p>So here&amp;rsquo;s the algorithm I have so far &amp;hellip;&lt;/p>
&lt;p>First, we need a way to figure out if a name is in Latin script or not. A name is Latin (for my
purposes) if it matches this regex: &lt;code>\A[\p{Latin}&amp;amp;\P{L}]+\z&lt;/code>. The &lt;code>\p{Latin}&lt;/code> matches any Unicode
character marked as being part of the Latin script&lt;sup id="fnref:6">&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref">6&lt;/a>&lt;/sup>. This includes ASCII letters, but also things
like &amp;ldquo;ą&amp;rdquo;, &amp;ldquo;Ň&amp;rdquo;, etc. The &lt;code>\P{L}&lt;/code> matches any character that is not a letter, including numbers,
emoji, etc. These two matches are combined together into a single character set. This is a pretty
good heuristic and will get it right for pretty much anything that an English reader can pronounce
(or reasonably mangle), including names like &amp;ldquo;Björk&amp;rdquo; or &amp;ldquo;Sigur Rós&amp;rdquo;, while not matching things we
can&amp;rsquo;t pronounce, like &amp;ldquo;緑黄色社会&amp;rdquo; or &amp;ldquo;Чайковский&amp;rdquo; (Tchaikovsky).&lt;/p>
&lt;p>The only place this goes wrong is names that include non-Latin characters for emoticons like
&amp;ldquo;(┛◉Д◉)┛彡┻━┻&amp;rdquo;. While it does include a Cyrillic character, &amp;ldquo;Д&amp;rdquo;, and a Chinese radical, &amp;ldquo;彡&amp;rdquo;, it is
not in a foreign language and doesn&amp;rsquo;t need to be transcribed or translated (though it&amp;rsquo;s also
completely unpronounceable). Looking at the MB data, this &lt;em>is&lt;/em> a thing. There&amp;rsquo;s an artist named
&amp;ldquo;（´・д・）ﾉ&amp;rdquo; in the database. I&amp;rsquo;m not sure how you&amp;rsquo;d pronounce this. Treating this as non-Latin is
fine. If the artist has an alternate alias that&amp;rsquo;s speakable, then it&amp;rsquo;d be good to have that, and if
they don&amp;rsquo;t, that&amp;rsquo;s okay.&lt;/p>
&lt;p>I start by pulling all the possible names from the MB data and categorizing them into three types:&lt;/p>
&lt;ul>
&lt;li>The Primary name, which is the name attached to the artist record, as opposed to aliases.&lt;/li>
&lt;li>Aliases which aren&amp;rsquo;t search hints. Names used in artist credits are also put in this category.&lt;/li>
&lt;li>Search hints.&lt;/li>
&lt;/ul>
&lt;p>I have a special case for the fact that MB uses Latin script sortable names for non-Latin names. In
some cases the only instance of a Latin name is as the primary sortable name. In that case, I add
that sortable name to my list of names as an alias. To make things trickier, this sortable name may
be something like &amp;ldquo;Asai, Kenichi&amp;rdquo;, so I transform it back to &amp;ldquo;Kenichi Asai&amp;rdquo; if it contains &amp;ldquo;, &amp;quot; (a
comma followed by a space)&lt;sup id="fnref:7">&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref">7&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>Once I have all these names I have to figure out the best primary name and sortable name,
transcribed name and sortable name, and translated name and sortable name. Of these, only the
primary name is required. Everything else is optional.&lt;/p>
&lt;p>I should only have one primary name. If this name is Latin, I use that along with the corresponding
primary sortable name. If the primary name is &lt;em>not&lt;/em> Latin but its sortable name &lt;em>is&lt;/em> Latin, then I
use the name but not the sortable name.&lt;/p>
&lt;p>It&amp;rsquo;s very important to take the primary name as given by the MB data. Originally I tried preferring
non-Latin names, but that doesn&amp;rsquo;t work at all. There are lots of Japanese bands whose canonical
names are in English, like &lt;a href="http://pillows.jp/">the pillows&lt;/a> and
&lt;a href="https://www.thebackhorn.com/">The Back Horn&lt;/a>. While these bands do have Japanese aliases, their
correct name is in English. There&amp;rsquo;s also even weirder cases like a band named
&lt;a href="https://www.moools.com/">moools&lt;/a>, which is not exactly in English but is still always written in
Latin script.&lt;/p>
&lt;p>If the primary name is not Latin, then I look at the remaining names to try to figure out whether
there are transcribed and/or translated names that can be used.&lt;/p>
&lt;p>To do that I first filter out all the names that match the primary name and sortable name, as well
as any names not in Latin script. Then I go through whatever&amp;rsquo;s left and split each name into words.
For each word, I look up the word in a dictionary (right now a copy of &lt;code>/usr/share/dict/words&lt;/code>) and
count how many words in the name are in our dictionary&lt;sup id="fnref:8">&lt;a href="#fn:8" class="footnote-ref" role="doc-noteref">8&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>If there&amp;rsquo;s only one name, then if it contains any known words it&amp;rsquo;s a translation, otherwise it&amp;rsquo;s a
transcription.&lt;/p>
&lt;p>If we have multiple possible names to choose from, I use the known word counts to (attempt to)
distinguish a transcription from a translation. A name with more known words from the dictionary is
more likely to be a translation. So if we have many possibilities, the one with the most known words
is a translation, and everything else is a transcription. This part of the algorithm works well for
band names.&lt;/p>
&lt;p>As an illustration of how this works, let&amp;rsquo;s take one of my favorite bands,
&lt;a href="https://www.tokyojihen.com/">東京事変&lt;/a>. Their name translates to &amp;ldquo;Tokyo Incidents&amp;rdquo;, and the
transcription is &amp;ldquo;Tokyo Jihen&amp;rdquo;. Because &amp;ldquo;Tokyo&amp;rdquo; and &amp;ldquo;Incidents&amp;rdquo; are both in the word list, the
algorithm picks &amp;ldquo;Tokyo Incidents&amp;rdquo; as the translation, with two known words, and &amp;ldquo;Tokyo Jihen&amp;rdquo; as the
transcription, with just one known word.&lt;/p>
&lt;p>This works less well for person names, since in that case I really don&amp;rsquo;t want to pick a translated
name at all. There will be cases where a person&amp;rsquo;s name transcription ends up containing an english
word, a Chinese name that transcribes as &amp;ldquo;Hui Ping&amp;rdquo;. A future revision will probably want to look at
whether it&amp;rsquo;s dealing with a person or entity.&lt;/p>
&lt;p>This gives me a list of possible transcriptions and translations, each of which is a name plus an
optional sortable name. To pick the best one from each, I sort each list. If the name came from an
alias for the &lt;code>en&lt;/code> locale, I prefer that. If multiple names came from that locale, I tiebreak by
looking at whether one was marked as the primary alias for that locale. If &lt;em>that&lt;/em> doesn&amp;rsquo;t work, I
prefer names that also have sortable names. And if there are still ties, I sort by string length and
then just sort the names, in order to ensure that the algorithm picks the same name every time given
the same inputs.&lt;/p>
&lt;p>Everything that isn&amp;rsquo;t picked is stored as a search hint, so I don&amp;rsquo;t have to remember the name the
system chose for an artist.&lt;/p>
&lt;p>Phew, that&amp;rsquo;s a lot!&lt;/p>
&lt;p>I really enjoy working on this sort of problem, where we have messy data and have to find a best
path through to get the results we want.&lt;/p>
&lt;p>But I originally started working on this (weeks ago) to do a quick version of the data importer so I
could whip up a quick backend so I could get back to where I started, which was the frontend. I had
put the frontend on hold because I felt like I needed more real data to work with in order to make
more progress on the frontend. Now my importer is capable of importing my entire music collection,
so it&amp;rsquo;s time to whip up that backend and get back to the UI work.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>I will probably release the code for this at some point but for now it&amp;rsquo;s in a private repo.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>The topic of how and why there&amp;rsquo;s so much amazing Japanese music as compared to non-Japanese
music is an interesting one that I&amp;rsquo;m only sort of qualified to write about. Maybe I&amp;rsquo;ll get into
this in a future blog post.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>I currently use Rhythmbox on my computer and YouTube Music on my phone. Both could be better.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>He&amp;rsquo;s a fantastic guitarist and songwriter. Here&amp;rsquo;s
&lt;a href="https://youtu.be/5lWvh9Wh9nE">a video for one of his songs&lt;/a>. Also, as an aside, the drummer has
a right handed drum kit but is playing the hi-hat with his left hand, which is really weird to
see.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>Hiragana is one of two Japanese syllabaries (like an alphabet but the components are syllables,
not letters). It is used for Japanese words and Katakana, the other syllabary, is used for
foreign words. In Japanese, word(s) are often sorted by their pronunciation rather than some
more abstract system based on the properties of the kanji used to write the word(s)&lt;sup id="fnref:9">&lt;a href="#fn:9" class="footnote-ref" role="doc-noteref">9&lt;/a>&lt;/sup>.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:6">
&lt;p>Thank dog that Unicode characters have so much metadata! Without this I&amp;rsquo;m not sure how I&amp;rsquo;d test
if a name is Latin or not.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:7">
&lt;p>I should probably &lt;em>not&lt;/em> do this if there are two commas to handle band names that are lists,
like if &amp;ldquo;Earth, Wind &amp;amp; Fire&amp;rdquo; used an Oxford comma, which they don&amp;rsquo;t. But the perfect is the
enemy of the good, and I want to get a good enough version going so I can work on other things.&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:8">
&lt;p>If a word contains a hypen I will also split that up and check the subparts to see if they&amp;rsquo;re
words. This would handle something like &amp;ldquo;Green-Yellow&amp;rdquo;, which isn&amp;rsquo;t a word, but is clearly made
of words.&amp;#160;&lt;a href="#fnref:8" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:9">
&lt;p>Sorting based on abstract properties is exactly how Chinese is sorted, since Chinese doesn&amp;rsquo;t
really have a syllabary (except it does, but people don&amp;rsquo;t use the syllabaries for day to day
stuff very much, unlike in Japan).&amp;#160;&lt;a href="#fnref:9" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>What Do I Want from My Next Job?</title><link>https://blog.urth.org/2021/10/15/what-do-i-want-from-my-next-job-/</link><pubDate>Fri, 15 Oct 2021 10:25:12 -0500</pubDate><guid>https://blog.urth.org/2021/10/15/what-do-i-want-from-my-next-job-/</guid><description>&lt;p>I&amp;rsquo;m currently enjoying &lt;a href="https://blog.urth.org/2021/10/07/let-the-funemployment-begin/">being unemployed&lt;/a>, and I won&amp;rsquo;t be looking for a new position until some
time in 2022, but I&amp;rsquo;ve been thinking about what I want from next position. There are many things I&amp;rsquo;d
&lt;em>like&lt;/em>, but what are my priorities?&lt;/p>
&lt;p>&lt;strong>Update 2022: The more I&amp;rsquo;ve thought about this the more I&amp;rsquo;ve realized that I can achieve my goal of
working less by aiming for higher compensaion &lt;em>now&lt;/em> in order to be able work less in the future. All
of which is to say that I&amp;rsquo;m no longer committed to finding a job with a 4-day week.&lt;/strong>&lt;/p>
&lt;p>This post is my attempt to organize my own thoughts about this so I can be more careful and
systematic when I start looking for my next position.&lt;/p>
&lt;h2 id="working-less">Working Less&lt;/h2>
&lt;p>I&amp;rsquo;ll be 48 by the time I start my next job. Realistically, that means my life is more than half
over. While I don&amp;rsquo;t hate working, I don&amp;rsquo;t love it so much that I want to maximize the amount of time
I spend doing it! So spending less time at work is my top priority.&lt;/p>
&lt;p>&lt;strong>My number one criteria for my next job is being able to work 4 days per week (or less)&lt;/strong>. I&amp;rsquo;m
talking about 4 eight hour days, not 4 ten hours days&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>. A 3 day week would be even more amazing,
but at that point it&amp;rsquo;s really a part time job. I don&amp;rsquo;t think I can find that except as a consultant
and I don&amp;rsquo;t want to go back to consulting.&lt;/p>
&lt;p>Fortunately, the 4 day work week seems to be getting a bit of traction recently. More and more
companies are embracing this, though the absolute numbers are still small. I&amp;rsquo;ve found a couple job
boards that make finding these positions easier, &lt;a href="https://4dayweek.io/">4dayweek.io&lt;/a> and
&lt;a href="https://peoplefirstjobs.com/">People-First Jobs&lt;/a>.&lt;/p>
&lt;p>And even if a company isn&amp;rsquo;t advertising a 4 day week, sometimes you can negotiate for this
individually. At my previous job, I had a 9 day fortnight (every other Friday off). And the job
before that I did have a 4 day week. In both cases, this was something I negotiated, not a company
policy.&lt;/p>
&lt;p>&lt;strong>I also want at least 5 weeks of PTO per year&lt;/strong>. Fortunately, this one seems much easier to find.
Given the incredibly hot labor market in tech right now, I&amp;rsquo;m seeing many positions that offer this.&lt;/p>
&lt;p>Of course, some places also offer the nebulous &amp;ldquo;unlimited vacation&amp;rdquo;. In the best case, employers do
this so they can let people who want more time off have it without accruing a huge liability in
untaken PTO for people who don&amp;rsquo;t.&lt;/p>
&lt;p>But the devil is in the details. Clearly it&amp;rsquo;s not actually unlimited, since otherwise I could simply
take all my time off and never work. So what does &amp;ldquo;unlimited&amp;rdquo; really mean? My plan is to ask
everyone I talk to a few questions about this:&lt;/p>
&lt;ul>
&lt;li>How much time did you take off over the past 12 months?&lt;/li>
&lt;li>How much time did your coworkers and/or reports take off over the past 12 months?&lt;/li>
&lt;li>How much time did members of upper management take off over the past 12 months?&lt;/li>
&lt;li>What do you think the real upper limit on PTO is? (Because &lt;em>of course&lt;/em> there is one!)&lt;/li>
&lt;/ul>
&lt;p>Some companies that offer unlimited vacation also have a mandatory minimum amount of vacation, which
I think is a very good sign, as long as it&amp;rsquo;s enforced.&lt;/p>
&lt;h2 id="compensation">Compensation&lt;/h2>
&lt;p>If working less is my top priority, I may have to compromise a bit on compensation. None of the the
very top-paying companies offer 4 day weeks yet (AFAIK). But again, given the incredibly hot labor
market in tech, I suspect I can find something that pays as much or more than my last position.&lt;/p>
&lt;p>I&amp;rsquo;d love to share what my actual compensation at past positions was, because I think people don&amp;rsquo;t
talk about this nearly enough. But sharing this information with all potential future employers
seems like a tactical mistake. I compromised by putting my salary information into
&lt;a href="https://www.levels.fyi/">levels.fyi&lt;/a>. But given how small
&lt;a href="https://www.activestate.com/">ActiveState&lt;/a> is, it may be quite some time before they have enough
data points to show their salaries.&lt;/p>
&lt;h2 id="languages-and-technology">Languages and Technology&lt;/h2>
&lt;p>I would really love to work with &lt;a href="https://www.rust-lang.org/">Rust&lt;/a>. I&amp;rsquo;ve been using it for personal
projects for a while and I find it quite satisfying. That said, Rust jobs are rare, and many of them
are either in fields I want to avoid, like cryptocurrency (see below), or they require expertise I
don&amp;rsquo;t have, like graphics or low-level programming experience.&lt;/p>
&lt;p>More generally, I&amp;rsquo;d really like to do something where I can learn something new. Building yet
another REST app in a dynamic language or Go does not feel very exciting at this stage in my career.&lt;/p>
&lt;p>Also, I won&amp;rsquo;t do Java (other JVM languages are fine) or PHP.&lt;/p>
&lt;h2 id="company-size-and-stage">Company Size and Stage&lt;/h2>
&lt;p>In my career so far, I&amp;rsquo;ve found that I&amp;rsquo;ve most enjoyed working for a small company that is already
profitable. That said, I&amp;rsquo;ve never worked for a startup that actually took off, nor have I ever
worked for a large tech-focused company (like a FAANG, Stripe, Elastic, etc.). I think either of
those could be really good experiences as well.&lt;/p>
&lt;p>Surprisingly, I have seen a few startups offering 4 day weeks. My question is whether they end up in
some sort of indefinite crunch mode and throw this out the window because of the pressure that a
startup experiences.&lt;/p>
&lt;p>All of this is to say that I&amp;rsquo;d consider just about anything &lt;em>except&lt;/em> for a large company that isn&amp;rsquo;t
a tech company. I did that once, and it was by far the worst work experience I&amp;rsquo;ve had. Never again!&lt;/p>
&lt;h2 id="ic1-or-management">IC&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> or Management?&lt;/h2>
&lt;p>At ActiveState, my most recent employer, I was as Team Lead, which included actual people management
responsibilities. I had never really done this before, so I decided to take the position in order to
try something new and do something that scared me. Challenges are good.&lt;/p>
&lt;p>I enjoyed this position. My team size varied from 2-5 people over time based on restructurings,
people leaving, etc. As a rough estimate, I&amp;rsquo;d say I spent 40% of my time on management stuff and 60%
doing IC work like writing design docs, coding, code reviews, etc. This was a good balance for me.&lt;/p>
&lt;p>I think I did a reasonably good job at management, though it was definitely a big learning curve.
The people who worked for me gave me very positive reviews. The people above me also gave me good
reviews, though they had more constructive criticism than my reports.&lt;/p>
&lt;p>So what should I do next?&lt;/p>
&lt;p>I&amp;rsquo;m pretty sure I wouldn&amp;rsquo;t enjoy being any higher up in the management chain. At any level above
Team Lead, you have even less time for IC work. My manager, the Director of Engineering, did find
some time to code, but not a lot of it. The CTO had even less, to the point where he mostly seemed
to write what code he did in the evening or on weekends.&lt;/p>
&lt;p>I&amp;rsquo;d be happy with a similar level of management responsibilities in the future. But I would also
enjoy a higher level IC position (Staff/Principal/Grand Poobah Engineer). Strategically, the IC
position might be a better choice, because I don&amp;rsquo;t want to advance any further up the management
track than where I&amp;rsquo;d start.&lt;/p>
&lt;h2 id="companys-product">Company&amp;rsquo;s Product&lt;/h2>
&lt;p>I&amp;rsquo;m not super picky about the product. I think anything can be interesting and rewarding, especially
if you have engaged customers who really like what you&amp;rsquo;re building. A developer-focused product is
always fun, and I enjoyed that aspect of ActiveState, but it&amp;rsquo;s not a necessity for me.&lt;/p>
&lt;p>But there are a few things I &lt;strong>don&amp;rsquo;t&lt;/strong> want to work on:&lt;/p>
&lt;h3 id="cryptocurrency">Cryptocurrency&lt;/h3>
&lt;p>I don&amp;rsquo;t know enough about this field to distinguish scams from real products, and it&amp;rsquo;s really hard
for me to see how the upsides of these products outweigh their downsides.&lt;/p>
&lt;h3 id="surveillance-capitalism-and-social-networking">Surveillance Capitalism and Social Networking&lt;/h3>
&lt;p>Just no.&lt;/p>
&lt;h3 id="gig-economy-work-for-non-professionals">Gig Economy Work for Non-Professionals&lt;/h3>
&lt;p>Again, just say no. Everything I know about Uber/Lyft/Doordash/GrubHub/etc. is bad. They are
predatory companies taking brutal advantage of people who are cash-poor and not great at math. Plus
they seem to have no viable way of ever being profitable, so they&amp;rsquo;re basically just a scam to funnel
VC&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> money to founders, or from early VCs to later VCs&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>In my personal life, I&amp;rsquo;ve sworn off all food delivery services, and I&amp;rsquo;m doing my best to avoid
&amp;ldquo;rideshare&amp;rdquo;&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup> services too.&lt;/p>
&lt;p>I specify &amp;ldquo;non-professionals&amp;rdquo; because there are also gig products aimed at professionals, like
graphic designers, software developers, etc. I&amp;rsquo;m not sure whether these products are purely bad in
the same way, and I can imagine a product that was actually a net win for everyone involved.&lt;/p>
&lt;h3 id="what-else">What Else?&lt;/h3>
&lt;p>I feel like I&amp;rsquo;m probably missing some other field. I haven&amp;rsquo;t seen any MLM&lt;sup id="fnref:6">&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref">6&lt;/a>&lt;/sup> companies advertising
for developers, but that would be a hard no if it came up.&lt;/p>
&lt;h2 id="narrowing-it-down">Narrowing it Down&lt;/h2>
&lt;p>My hard requirements are:&lt;/p>
&lt;ul>
&lt;li>A 4 day week (or less).&lt;/li>
&lt;li>5+ weeks of PTO.&lt;/li>
&lt;li>No Java or PHP.&lt;/li>
&lt;li>No companies in the unacceptable fields I&amp;rsquo;ve mentioned.&lt;/li>
&lt;li>I want to do a significant amount of IC work, even if I&amp;rsquo;m in a management position.&lt;/li>
&lt;/ul>
&lt;p>Everything else is a negotiation or compromise.&lt;/p>
&lt;p>This is a post that makes me wish I had a commenting solution for this blog. But I&amp;rsquo;ll share this on
Hacker News and hopefully I&amp;rsquo;ll get some good feedback there.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>As a software developer, you&amp;rsquo;re not going to get more done in a ten hour day anyway!&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>IC = individual contributor, aka not management&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>VC = venture capital&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>Though given how much funding goes into these companies, the VCs must think there is a way to
make money here.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>Why is this called &amp;ldquo;rideshare&amp;rdquo;? What is being shared?&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:6">
&lt;p>MLM = multi-level marketing&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Let the Funemployment Begin</title><link>https://blog.urth.org/2021/10/07/let-the-funemployment-begin/</link><pubDate>Thu, 07 Oct 2021 15:52:23 -0500</pubDate><guid>https://blog.urth.org/2021/10/07/let-the-funemployment-begin/</guid><description>&lt;p>Today was my last day at &lt;a href="https://www.activestate.com/">ActiveState&lt;/a>. I enjoyed my nearly five years
there (starting in February of 2017), but for a variety of reasons I decided to leave. I&amp;rsquo;m in the
very fortunate position of being able to be jobless for a while, I&amp;rsquo;m not planning to look for
anything new until January of 2022 at the earliest.&lt;/p>
&lt;p>I have some programming projects of my own that I plan to work on, and I&amp;rsquo;ll post about them here if
they turn into anything interesting. I&amp;rsquo;m also maybe open to short-term consulting gigs (a few days
or weeks, not months) if someone wants to throw sufficiently large amounts of money at me.
&lt;a href="mailto:autarch@urth.org">Email me&lt;/a> if you&amp;rsquo;re interested.&lt;/p></description></item><item><title>Writing a Postgres SQL Pretty Printer in Rust: Part 2</title><link>https://blog.urth.org/2021/04/24/writing-a-postgres-sql-pretty-printer-in-rust-part-2/</link><pubDate>Sat, 24 Apr 2021 13:00:33 -0500</pubDate><guid>https://blog.urth.org/2021/04/24/writing-a-postgres-sql-pretty-printer-in-rust-part-2/</guid><description>&lt;p>It&amp;rsquo;s been a few weeks since my last post on this project. I was distracted by &lt;a href="https://blog.urth.org/2021/03/27/down-the-golang-nil-rabbit-hole/">Go
reflection&lt;/a> and &lt;a href="https://blog.urth.org/2021/03/29/security-issues-in-perl-ip-address-distros/">security issues with
Perl IP address modules&lt;/a>. But now I can get back to my Postgres
SQL pretty printer project&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>One of the challenges for this project has been figuring out the best way to test it. I tried a
standard unit test approach before giving up and settling on integration testing instead, so in this
post I&amp;rsquo;ll talk about what I tried and what I ended up with.&lt;/p>
&lt;h2 id="series-links">Series Links&lt;/h2>
&lt;ul>
&lt;li>Part 1: &lt;a href="https://blog.urth.org/2021/03/14/writing-a-postgres-sql-pretty-printer-in-rust-part-1/">Introduction to the project and generating Rust with Perl&lt;/a>&lt;/li>
&lt;li>Part 1.5: &lt;a href="https://blog.urth.org/2021/03/21/writing-a-postgres-sql-pretty-printer-in-rust-part-1-5/">More about enum wrappers and Serde&amp;rsquo;s externally tagged enum representation&lt;/a>&lt;/li>
&lt;li>Part 2: &lt;strong>How I&amp;rsquo;m testing the pretty printer and how I generate tests from the Postgres docs&lt;/strong>&lt;/li>
&lt;/ul>
&lt;h2 id="unit-tests">Unit Tests&lt;/h2>
&lt;p>Whenever possible, I prefer to write unit tests for my code. Let&amp;rsquo;s define &amp;ldquo;unit test&amp;rdquo;, since people
use that term all the time, but with slightly different definitions&lt;/p>
&lt;p>When I say &amp;ldquo;unit tests&amp;rdquo; I&amp;rsquo;m referring to tests that test the functionality of a single module&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>,
focusing on its public API. If that module integrates with other modules, unit tests may require
mocking. Some will say it &lt;em>always&lt;/em> requires mocking, but I think that is often a waste of effort, so
I avoid mocking in many cases. And though I said &amp;ldquo;focusing on its public API&amp;rdquo;, sometimes I will test
private functions directly, if they are complex enough to warrant this and doing so is not too
painful.&lt;/p>
&lt;p>Unit tests are great when you can write them. They let you focus on one small piece of a code base
at a time to make sure that it does what you expect. Good unit tests cover the standard use cases,
corner cases (zero values, extreme values, etc.), various permutations of argument combinations, and
error handling.&lt;/p>
&lt;p>If you unit test all of your modules, you know that each module probably does what you want it to.
This doesn&amp;rsquo;t tell you whether they all work together properly, but it&amp;rsquo;s a good start.&lt;/p>
&lt;p>The bulk of the (non-generated) code for pg-pretty lives in one library crate named
&lt;a href="https://github.com/houseabsolute/pg-pretty/tree/master/formatter">&lt;code>formatter&lt;/code>&lt;/a>, and initially this
was all in the
&lt;a href="https://github.com/houseabsolute/pg-pretty/blob/master/formatter/src/lib.rs">crate root&amp;rsquo;s &lt;code>lib.rs&lt;/code> file&lt;/a>,
though I&amp;rsquo;m working on rewriting this in a branch named &lt;code>new-formatter&lt;/code> (no links because the branch
will be deleted after I merge it to master).&lt;/p>
&lt;p>The original formatter code&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> has many functions, but the vast majority of them work the same way.
They take a struct&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup> or slice of structs defined in the
&lt;a href="https://github.com/houseabsolute/pg-pretty/blob/master/parser/src/ast.rs">parser crate&amp;rsquo;s ast module&lt;/a>
and return a string representing the formatted SQL for that piece of the AST&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>Here&amp;rsquo;s a simple function from that code:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fn&lt;/span> &lt;span class="nf">format_range_var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">r&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="nc">RangeVar&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="nb">String&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">mut&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">names&lt;/span>: &lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">String&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="fm">vec!&lt;/span>&lt;span class="p">[];&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">c&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">catalogname&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">names&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">clone&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">schemaname&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">names&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">clone&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">names&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">relname&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">clone&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">mut&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">names&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">iter&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">map&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">Self&lt;/span>::&lt;span class="n">maybe_quote&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">collect&lt;/span>::&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">String&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;.&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">AliasWrapper&lt;/span>::&lt;span class="n">Alias&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">alias&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">push_str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">Self&lt;/span>::&lt;span class="n">alias_name&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">aliasname&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// XXX - do something with colnames here?
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>A &lt;code>RangeVar&lt;/code> is a struct representing a name in a &lt;code>FROM&lt;/code> clause. This will always have a &lt;code>relname&lt;/code>
(a table, view, or subselect), with optional database and schema names, like
&lt;code>some_db.some_schema.some_table&lt;/code>. In addition, it may have an alias.&lt;/p>
&lt;p>This is a relatively simple example, as this type of struct doesn&amp;rsquo;t contain any complex structs
itself. Other functions, like for formatting a select statement, mostly consist of calls to other
formatting functions:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fn&lt;/span> &lt;span class="nf">format_select_stmt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="k">mut&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="nc">SelectStmt&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="nc">R&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">match&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">target_list&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tl&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">tl&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Err&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Error&lt;/span>::&lt;span class="n">NoTargetListForSelect&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">mut&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">select&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">format_select_clause&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">f&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">from_clause&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">select&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">push_str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">format_from_clause&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">f&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">w&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">where_clause&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">select&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">push_str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">format_where_clause&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">w&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">g&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">group_clause&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">select&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">push_str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">format_group_by_clause&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">g&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">o&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">sort_clause&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">select&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">push_str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">format_order_by_clause&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">o&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nb">Ok&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">select&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This is an early, incomplete version which doesn&amp;rsquo;t handle &lt;code>HAVING&lt;/code> clauses, &lt;code>LIMIT&lt;/code> clauses, window
clauses, locking clauses, &lt;code>UNION&lt;/code> queries, etc. The point I&amp;rsquo;m trying to make is that these functions
get complex quickly. While breaking them down into lots of smaller functions helps, I&amp;rsquo;ve ended up
with a huge number of small functions.&lt;/p>
&lt;p>So how do we test this with unit tests? To do that, we need a way to produce structs from the &lt;code>ast&lt;/code>
module like &lt;code>RangeVar&lt;/code> or &lt;code>SelectStmt&lt;/code>. For reference, here&amp;rsquo;s &lt;code>RangeVar&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">struct&lt;/span> &lt;span class="nc">RangeVar&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// the catalog (database) name, or NULL
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">catalogname&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">String&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// char*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// the schema name, or NULL
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">schemaname&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">String&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// char*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// the relation/sequence name
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">relname&lt;/span>: &lt;span class="nb">String&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// char*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// expand rel by inheritance? recursively act
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// on children?
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">#[serde(default)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">inh&lt;/span>: &lt;span class="kt">bool&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// bool
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// see RELPERSISTENCE_* in pg_class.h
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">relpersistence&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="kt">char&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// char
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// table alias &amp;amp; optional column aliases
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">alias&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">AliasWrapper&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Alias*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// token location, or -1 if unknown
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">location&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="kt">i64&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// int
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The &lt;code>RangeVar&lt;/code> struct only refers to one other ast enum or struct, &lt;code>AliasWrapper&lt;/code>, which in turn
contains an &lt;code>Alias&lt;/code>. But the &lt;code>SelectStmt&lt;/code> is &lt;em>much&lt;/em> more complex&lt;sup id="fnref:6">&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref">6&lt;/a>&lt;/sup>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">struct&lt;/span> &lt;span class="nc">SelectStmt&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// NULL, list of DISTINCT ON exprs, or
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// lcons(NIL,NIL) for all (SELECT DISTINCT)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">#[serde(rename = &lt;/span>&lt;span class="s">&amp;#34;distinctClause&amp;#34;&lt;/span>&lt;span class="cp">)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">distinct_clause&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">List&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// List*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// target for SELECT INTO
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">#[serde(rename = &lt;/span>&lt;span class="s">&amp;#34;intoClause&amp;#34;&lt;/span>&lt;span class="cp">)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">into_clause&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">IntoClauseWrapper&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// IntoClause*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// the target list (of ResTarget)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">#[serde(rename = &lt;/span>&lt;span class="s">&amp;#34;targetList&amp;#34;&lt;/span>&lt;span class="cp">)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">target_list&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">List&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// List*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// the FROM clause
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">#[serde(rename = &lt;/span>&lt;span class="s">&amp;#34;fromClause&amp;#34;&lt;/span>&lt;span class="cp">)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">from_clause&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">List&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// List*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// WHERE qualification
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">#[serde(rename = &lt;/span>&lt;span class="s">&amp;#34;whereClause&amp;#34;&lt;/span>&lt;span class="cp">)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">where_clause&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">Box&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Node&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Node*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// GROUP BY clauses
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">#[serde(rename = &lt;/span>&lt;span class="s">&amp;#34;groupClause&amp;#34;&lt;/span>&lt;span class="cp">)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">group_clause&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">List&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// List*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// HAVING conditional-expression
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">#[serde(rename = &lt;/span>&lt;span class="s">&amp;#34;havingClause&amp;#34;&lt;/span>&lt;span class="cp">)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">having_clause&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">Box&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Node&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Node*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// WINDOW window_name AS (...), ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">#[serde(rename = &lt;/span>&lt;span class="s">&amp;#34;windowClause&amp;#34;&lt;/span>&lt;span class="cp">)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">window_clause&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">List&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// List*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// untransformed list of expression lists
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">#[serde(rename = &lt;/span>&lt;span class="s">&amp;#34;valuesLists&amp;#34;&lt;/span>&lt;span class="cp">)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">values_lists&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">List&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// List*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// sort clause (a list of SortBy&amp;#39;s)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">#[serde(rename = &lt;/span>&lt;span class="s">&amp;#34;sortClause&amp;#34;&lt;/span>&lt;span class="cp">)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sort_clause&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">SortByWrapper&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// List*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// # of result tuples to skip
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">#[serde(rename = &lt;/span>&lt;span class="s">&amp;#34;limitOffset&amp;#34;&lt;/span>&lt;span class="cp">)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">limit_offset&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">Box&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Node&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Node*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// # of result tuples to return
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">#[serde(rename = &lt;/span>&lt;span class="s">&amp;#34;limitCount&amp;#34;&lt;/span>&lt;span class="cp">)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">limit_count&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">Box&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Node&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// Node*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// FOR UPDATE (list of LockingClause&amp;#39;s)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">#[serde(rename = &lt;/span>&lt;span class="s">&amp;#34;lockingClause&amp;#34;&lt;/span>&lt;span class="cp">)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">locking_clause&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">LockingClauseWrapper&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// List*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// WITH clause
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">#[serde(rename = &lt;/span>&lt;span class="s">&amp;#34;withClause&amp;#34;&lt;/span>&lt;span class="cp">)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">with_clause&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">WithClauseWrapper&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// WithClause*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// type of set op
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">op&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">SetOperation&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// SetOperation
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// ALL specified?
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">#[serde(default)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">all&lt;/span>: &lt;span class="kt">bool&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// bool
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// left child
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">larg&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">Box&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">SelectStmtWrapper&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// SelectStmt*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// right child
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rarg&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">Box&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">SelectStmtWrapper&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// SelectStmt*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Besides having many more fields than &lt;code>RangeVar&lt;/code>, most of those fields are other types of ast nodes.
In fact, many of these are a boxed &lt;code>Node&lt;/code> or a &lt;code>List&lt;/code>, which is a &lt;code>Vec&amp;lt;Node&amp;gt;&lt;/code>. One field,
&lt;code>values_lists&lt;/code>, is a &lt;code>Vec&amp;lt;List&amp;gt;&lt;/code>! A &lt;code>Node&lt;/code> is an enum which can be &lt;em>any&lt;/em> AST node&lt;sup id="fnref:7">&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref">7&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>So how exactly do we produce the various &lt;code>SelectStmt&lt;/code> structs that we&amp;rsquo;d want to feed into
&lt;code>format_select_stmt&lt;/code> for testing? The structs themselves have no constructors. The parser only
generates them based on the results of parsing, which is done in C code for which there is no public
API. That C code produces a JSON representation of the AST, which we deserialize into structs.&lt;/p>
&lt;p>So the only way left to do this is to construct them &amp;ldquo;by hand&amp;rdquo; with code like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fn&lt;/span> &lt;span class="nf">make_range_var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">c&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&amp;amp;&lt;/span>&lt;span class="kt">str&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&amp;amp;&lt;/span>&lt;span class="kt">str&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">r&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="kt">str&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&amp;amp;&lt;/span>&lt;span class="kt">str&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="nc">Node&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">alias&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">match&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">AliasWrapper&lt;/span>::&lt;span class="n">Alias&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Alias&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">aliasname&lt;/span>: &lt;span class="nc">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">to_string&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">colnames&lt;/span>: &lt;span class="nb">None&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">})),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">catalogname&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">match&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">c&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">c&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">to_string&lt;/span>&lt;span class="p">()),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">schemaname&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">match&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">to_string&lt;/span>&lt;span class="p">()),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Node&lt;/span>::&lt;span class="n">RangeVar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">RangeVar&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">catalogname&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">schemaname&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">relname&lt;/span>: &lt;span class="nc">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">to_string&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">inh&lt;/span>: &lt;span class="nc">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">relpersistence&lt;/span>: &lt;span class="nb">None&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">alias&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">location&lt;/span>: &lt;span class="nb">None&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">})&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This isn&amp;rsquo;t entirely terrible, but as I noted before, the &lt;code>RangeVar&lt;/code> struct is one of the simpler
structs in the AST. The equivalent for a &lt;code>SelectStmt&lt;/code> would be absolutely enormous. Even for a
&lt;code>RangeVar&lt;/code>, constantly having to pass mostly &lt;code>None&lt;/code> for the arguments gets old very fast. To make
this simpler, I made a macro:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="n">macro_rules&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">range_var&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$relname&lt;/span>:&lt;span class="nc">literal&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$(,&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">make_range_var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$relname&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$relname&lt;/span>:&lt;span class="nc">literal&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="no">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$alias&lt;/span>:&lt;span class="nc">literal&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">make_range_var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$relname&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="cp">$alias&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$catalogname&lt;/span>:&lt;span class="nc">literal&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$relname&lt;/span>:&lt;span class="nc">literal&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$(,&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">make_range_var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="cp">$catalogname&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$relname&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$catalogname&lt;/span>:&lt;span class="nc">literal&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$relname&lt;/span>:&lt;span class="nc">literal&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="no">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$alias&lt;/span>:&lt;span class="nc">literal&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">make_range_var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="cp">$catalogname&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$relname&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="cp">$alias&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$schemaname&lt;/span>:&lt;span class="nc">literal&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$catalogname&lt;/span>:&lt;span class="nc">literal&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$relname&lt;/span>:&lt;span class="nc">literal&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$(,&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">make_range_var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="cp">$schemaname&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="cp">$catalogname&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$relname&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$schemaname&lt;/span>:&lt;span class="nc">literal&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$catalogname&lt;/span>:&lt;span class="nc">literal&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$relname&lt;/span>:&lt;span class="nc">literal&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="no">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">$alias&lt;/span>:&lt;span class="nc">literal&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">make_range_var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="cp">$schemaname&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="cp">$catalogname&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="cp">$relname&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="cp">$alias&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This covers every possible combination of optional arguments, so I could write
&lt;code>range_var!(&amp;quot;people&amp;quot;)&lt;/code> or &lt;code>range_var!(&amp;quot;people&amp;quot; AS &amp;quot;persons&amp;quot;)&lt;/code> or
&lt;code>range_var!(&amp;quot;some_schema&amp;quot;, &amp;quot;people&amp;quot;)&lt;/code> and so on.&lt;/p>
&lt;p>This made the tests more concise, but the equivalent macro implementation for a &lt;code>SelectStmt&lt;/code> might
be hundreds or even thousands of lines long.&lt;/p>
&lt;h2 id="giving-up-on-unit-tests">Giving Up on Unit Tests&lt;/h2>
&lt;p>I quickly realized that this approach simply wouldn&amp;rsquo;t scale. The structs that the formatter deals
with are &lt;em>so complex&lt;/em>, there are &lt;em>so many&lt;/em> of them, and each struct has &lt;em>so many&lt;/em> possible
variations of optional fields, types of contained nodes, etc.&lt;/p>
&lt;p>With my macro plus function approach, I&amp;rsquo;d have to write tens of thousands of lines of code in
support of these unit tests. And that code would itself be so complex that it would really demand
its own test suite!&lt;/p>
&lt;p>But that&amp;rsquo;s not even the biggest problem. The biggest problem is that because these AST structs are
produced by the Postgres parser C code, I really have no idea what all the possibilities for a given
struct are. Given that many structs simply contain &lt;code>Node&lt;/code> or &lt;code>List&lt;/code> structs, the possibilities are
literally limitless.&lt;/p>
&lt;p>So in summary:&lt;/p>
&lt;ul>
&lt;li>Writing struct generation code would be much more work than writing the formatter.&lt;/li>
&lt;li>The struct generation code would be so complex that it&amp;rsquo;d require its own test suite.&lt;/li>
&lt;li>There&amp;rsquo;s no good way for me to know what structs to generate for tests without lots of real-world
examples.&lt;/li>
&lt;/ul>
&lt;p>That last bullet point mentions &amp;ldquo;lots of real-world examples&amp;rdquo;. That&amp;rsquo;s what I needed. So how to get
them? I could scour my own projects for examples, though in many cases I use an ORM, so it&amp;rsquo;s not a
trivial matter of just copying some SQL. I could look for projects on GitHub that use Postgres.
Maybe I could look on Stack Overflow.&lt;/p>
&lt;p>But finally, I had a good idea&lt;sup id="fnref:8">&lt;a href="#fn:8" class="footnote-ref" role="doc-noteref">8&lt;/a>&lt;/sup>. The Postgres documentation is quite extensive, and it includes
&lt;em>many&lt;/em> SQL examples! If I could extract those examples then those examples could form the basis of a
test suite.&lt;/p>
&lt;h2 id="integration-tests-to-the-rescue">Integration Tests to the Rescue&lt;/h2>
&lt;p>Taking a step back, I realized that what I really wanted to test was the formatter as a whole. While
unit tests are great, I could greatly simplify my test code by simply comparing SQL statement input
to SQL statement output. Any time my output diverged from what I expected, that&amp;rsquo;s a bug in the
formatter (or in my expectations). And if the formatter panics because it can&amp;rsquo;t handle a particular
node&lt;sup id="fnref:9">&lt;a href="#fn:9" class="footnote-ref" role="doc-noteref">9&lt;/a>&lt;/sup>, that&amp;rsquo;s a missing piece of the implementation.&lt;/p>
&lt;p>This was yet another case where Perl came in handy. I wrote a quick script to parse the entire
documentation tree&lt;sup id="fnref:10">&lt;a href="#fn:10" class="footnote-ref" role="doc-noteref">10&lt;/a>&lt;/sup> and find &lt;code>programlisting&lt;/code> elements which contained SQL. These are then put
into files named after the doc file that contained them, giving me files with content like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">++++
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">----
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">CREATE TABLE base_table (id int, ts timestamptz)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">----
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">???
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">----
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The first section, &lt;code>1&lt;/code>, is a test description, to be filled in later by me. The second section is
the input, and the third, &lt;code>???&lt;/code>, is the expected output. These generated files are useful seeds for
test cases, though I have to fill in the test name and expected output. An example from an actual
test looks like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">++++
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">SELECT FOR UPDATE in subselect
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">----
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">SELECT * FROM (SELECT * FROM mytable FOR UPDATE) ss ORDER BY column1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">----
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">SELECT *
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">FROM (
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> SELECT *
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> FROM mytable
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> FOR UPDATE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ) AS ss
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ORDER BY column1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">----
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;em>(I will be improving the formatting, because I don&amp;rsquo;t like the output the original version gives!)&lt;/em>&lt;/p>
&lt;p>These files are easy to edit, and adding new tests by hand is easy as well.&lt;/p>
&lt;p>The
&lt;a href="https://github.com/houseabsolute/pg-pretty/blob/master/formatter/tests/formatter.rs">test harness&lt;/a>
simply reads each file, splits them up into individual cases, and runs the input through the
formatter and compares it to the output.&lt;/p>
&lt;p>In order to make failures more understandable, I use
&lt;a href="https://lib.rs/crates/prettydiff">&lt;code>prettydiff&lt;/code>&lt;/a>&amp;rsquo;s
&lt;a href="https://docs.rs/prettydiff/0.4.0/prettydiff/text/fn.diff_lines.html">&lt;code>diff_lines&lt;/code> function&lt;/a> to
compare the expected output to what I actually got. This is quite helpful, but in cases where they
differ by whitespace, especially trailing newlines, it&amp;rsquo;s not as helpful as I&amp;rsquo;d like.&lt;/p>
&lt;p>So I also added some optional debugging output (based on an env var) that shows me each escaped
character in the expected versus actual input. This lets me easily see whitespace differences, and
newlines are printed as &lt;code>\n&lt;/code>, which makes extra newlines obvious as well.&lt;/p>
&lt;h2 id="in-summary">In Summary&lt;/h2>
&lt;p>Sometimes integration tests are better than unit tests. In this case, focusing on integration tests
freed me from a testing morass I was stuck in, letting me focus on the formatting code. The fact
that I can generate a huge number of integration tests gives me some confidence that this approach
will work.&lt;/p>
&lt;p>I definitely won&amp;rsquo;t get 100% test coverage (which is basically impossible given the recursive nature
of the fact that an AST &lt;code>Node&lt;/code>s can contain another &lt;code>Node&lt;/code>). But I think I can use this approach to
product a decent first release. Once people start using it, I will quickly get bug reports with more
test cases.&lt;/p>
&lt;h2 id="next-up">Next up&lt;/h2>
&lt;p>Here&amp;rsquo;s a list of what I want to cover in future posts.&lt;/p>
&lt;ul>
&lt;li>Diving into the Postgres grammar to understand the AST.&lt;/li>
&lt;li>The benefits of Rust pattern-matching for working with ASTs.&lt;/li>
&lt;li>How terrible my initial solution to generating SQL in the pretty printer is, and how I fixed it
(once I actually fix it).&lt;/li>
&lt;li>Who knows what else?&lt;/li>
&lt;/ul>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Try saying that three times fast.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>I&amp;rsquo;m referring to Rust modules here. Substitute package, library, etc. for your language of
choice.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>The examples are from
&lt;a href="https://github.com/houseabsolute/pg-pretty/tree/71b6e24a83d867900fb8c868b8501ff79e0f09f0">commit &lt;code>71b6e24&lt;/code>&lt;/a>.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>These are the structs that &lt;a href="https://blog.urth.org/2021/03/14/writing-a-postgres-sql-pretty-printer-in-rust-part-1/">I wrote about generating in Part 1&lt;/a>.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>Having each function return a string directly was the wrong approach. In my new branch I&amp;rsquo;m
having these functions return something else that is then formatted later. But more on that in a
future blog post once I&amp;rsquo;ve solidified this new approach.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:6">
&lt;p>You don&amp;rsquo;t need to read the entire struct definition. Just take note that this struct has a lot
of fields and move on.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:7">
&lt;p>In practice, these fields cannot really contain &lt;em>any&lt;/em> node. For example, the &lt;code>where_clause&lt;/code>
field, which is a &lt;code>Box&amp;lt;Node&amp;gt;&lt;/code>, is not going to contain an &lt;code>InsertStmt&lt;/code> or &lt;code>CreateDomainStmt&lt;/code> or
an &lt;code>IntoClause&lt;/code>. But this is what the underlying Postgres data structures use, and I have no
easy way of knowing &lt;em>which&lt;/em> subset of nodes a &lt;code>Node&lt;/code>-typed field will contain. In some cases,
I&amp;rsquo;ve actually figured this out through reading the Postgres parser code and my generator code
overrides the field definition to use a simpler type. I plan to write about how I do this in a
future blog post.&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:8">
&lt;p>This happens every once in a while. It&amp;rsquo;s always very exciting.&amp;#160;&lt;a href="#fnref:8" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:9">
&lt;p>There are &lt;em>so many&lt;/em> node types that I didn&amp;rsquo;t even try to list them all in the initial
&lt;code>format_node&lt;/code> implementation&amp;rsquo;s &lt;code>match&lt;/code>. Instead, I had
&lt;a href="https://github.com/houseabsolute/pg-pretty/blob/2272a2010ad214b98831535ce8732cc5c98fa2fd/formatter/src/lib.rs#L192">a default match (&lt;code>_&lt;/code>) that just returned an error&lt;/a>
saying that formatting for the given node type wasn&amp;rsquo;t implemented.&amp;#160;&lt;a href="#fnref:9" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:10">
&lt;p>The documentation files all have an &lt;code>sgml&lt;/code> extension so I used an SGML parser, but just now I
looked more closely and I think it&amp;rsquo;s mostly XML, but without a DTD in most files. Regardless, I
ended up using an SGML parser in my Perl code.&amp;#160;&lt;a href="#fnref:10" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>API Design and the Recent IP Address Module Issues</title><link>https://blog.urth.org/2021/04/03/api-design-and-the-recent-ip-address-module-issues/</link><pubDate>Sat, 03 Apr 2021 11:01:20 -0500</pubDate><guid>https://blog.urth.org/2021/04/03/api-design-and-the-recent-ip-address-module-issues/</guid><description>&lt;p>Earlier this week, I wrote about &lt;a href="https://blog.urth.org/2021/03/29/security-issues-in-perl-ip-address-distros/">security issues in Perl IP address distros&lt;/a>. I started thinking about &lt;em>why&lt;/em> these
issues showed up in so many distros, which got me thinking about how an API can make these types of
problems harder or easier.&lt;/p>
&lt;p>Specifically, I&amp;rsquo;d like to talk about
&lt;a href="https://metacpan.org/pod/Data::Validate::IP">&lt;code>Data::Validate::IP&lt;/code>&lt;/a>.&lt;/p>
&lt;p>Let&amp;rsquo;s look at two functions exported by this module, &lt;code>is_ipv4&lt;/code> and &lt;code>is_private_ipv4&lt;/code>.&lt;/p>
&lt;p>On the surface, these sure look like they&amp;rsquo;re the same general thing. They take a string and return a
boolean. But thinking about their semantics, these are very much &lt;em>not&lt;/em> the same general thing.&lt;/p>
&lt;p>The &lt;code>is_ipv4&lt;/code> function is &lt;strong>validating that a string is an IPv4 address&lt;/strong>.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-perl" data-lang="perl">&lt;span class="line">&lt;span class="cl">&lt;span class="n">is_ipv4&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#39;1.2.3.4&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1"># true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">is_ipv4&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#39;feed::3e&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1"># false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">is_ipv4&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#39;not even an IP address&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1"># false&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>As an aside, it actually returns the string you give it for a true value, but that will always be
treated as a true value by Perl.&lt;/p>
&lt;p>And it also returns false when given &lt;code>010.0.0.1&lt;/code>. This is (maybe) technically incorrect, but as we
saw from this week&amp;rsquo;s security issue, it&amp;rsquo;s probably better than returning true. If an attacker can
somehow supply this IP address to an application, or if someone just makes a typo in a config file,
this address can be treated as either &lt;code>10.0.0.1&lt;/code> or &lt;code>8.0.0.1&lt;/code>, depending on the code in question.&lt;/p>
&lt;p>This all seems great so far. So what&amp;rsquo;s the problem?&lt;/p>
&lt;p>Well, let&amp;rsquo;s think about &lt;code>is_private_ipv4&lt;/code>. Here&amp;rsquo;s what it returns for some inputs:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-perl" data-lang="perl">&lt;span class="line">&lt;span class="cl">&lt;span class="n">is_private_ipv4&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#39;10.0.0.1&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1"># true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">is_private_ipv4&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#39;010.0.0.1&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1"># false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">is_private_ipv4&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#39;1.2.3.4&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1"># false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">is_private_ipv4&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#39;feed::3e&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1"># false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">is_private_ipv4&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#39;not even an IP address&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1"># false&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>So what is this function doing? Well, it&amp;rsquo;s obviously doing IPv4 address validation, since it returns
false for things like &lt;code>feed::3e&lt;/code> or &lt;code>not even an IP address&lt;/code>. But &lt;strong>it&amp;rsquo;s also doing
categorization&lt;/strong>, because it returns false for a valid IP address like &lt;code>1.2.3.4&lt;/code>, while &lt;code>10.0.0.1&lt;/code>,
also a valid IPv4 address, returns true.&lt;/p>
&lt;p>So this function does two things, validation &lt;em>and&lt;/em> categorization, but the return value lumps these
things together. You cannot tell by its return value whether the address was invalid or if it was
valid but not private.&lt;/p>
&lt;p>The &lt;code>is_public_ipv4&lt;/code> function has the same problem. It does both validation and categorization in
one call.&lt;/p>
&lt;p>This is a very subtle point, and it&amp;rsquo;s easy to miss when you&amp;rsquo;re using this module. It would be very
easy to introduce a security issue with this code&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-perl" data-lang="perl">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="n">is_public_ipv4&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$some_addr&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">send_private_data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$some_addr&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">send_public_data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$some_addr&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If &lt;code>is_public_ipv4&lt;/code> is given &lt;code>010.0.0.1&lt;/code> it returns false, which means we send private data. So how
should this be written? We need to validate first:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-perl" data-lang="perl">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">die&lt;/span> &lt;span class="s">&amp;#34;Invalid IPv4: $some_addr&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">unless&lt;/span> &lt;span class="n">is_ipv4&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$some_addr&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="n">is_public_ipv4&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$some_addr&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">send_private_data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$some_addr&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">send_public_data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$some_addr&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Perhaps elsewhere in this code we might want to call &lt;code>is_linklocal_ipv4()&lt;/code> or
&lt;code>is_loopback_ipv4($ip)&lt;/code>. But we need to remember to add an &lt;code>is_ipv4&lt;/code> check before &lt;em>every&lt;/em>
&lt;code>is_*_ipv4&lt;/code> call. Will we remember? Probably not.&lt;/p>
&lt;p>While I&amp;rsquo;ve used this module for years, and I&amp;rsquo;ve even been its primary maintainer for some time, I
didn&amp;rsquo;t think about the implications of its API until earlier this week!&lt;/p>
&lt;p>So if the maintainer didn&amp;rsquo;t think about it, we can probably assume that most of its other users
didn&amp;rsquo;t either.&lt;/p>
&lt;p>What would a better API look like? We need to separate validation and categorization, and we need to
&lt;em>force&lt;/em> users to go through validation before doing categorization.&lt;/p>
&lt;p>There are various ways to do this, but an OO interface makes this trivial:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-perl" data-lang="perl">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nn">IPv4&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$some_addr&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">is_public&lt;/span> &lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">send_private_data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$some_addr&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">send_public_data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$some_addr&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If the &lt;code>IPv4-&amp;gt;new&lt;/code> call throws an exception on invalid data, then this code is perfectly safe&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>.
There is no way to use this API to categorize invalid data. So even the person who wrote this
terrible logic (&amp;ldquo;if not public send private?&amp;rdquo; WTF?!) will be prevented from doing more damage.&lt;/p>
&lt;p>Another approach would be to have &lt;code>is_private_ipv4&lt;/code> throw an exception if given invalid data. That
way it has three &amp;ldquo;return values&amp;rdquo;, true (valid and private), false (valid but not private), and
exception (invalid).&lt;/p>
&lt;p>Data validation is important for correctness, and correctness is important for security. Don&amp;rsquo;t
design APIs that put the validation burden on the user. Make it as hard as you can to do the wrong
thing with your API&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Yes, this code is bad, but that&amp;rsquo;s kind of the point. Is all the code you&amp;rsquo;ve ever worked with
well thought out and clearly structured? Did it always handle all the corner cases properly? Was
it always free from obvious logic errors? I can wait for you to stop laughing before we
continue.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>At least it&amp;rsquo;s safe if every address that&amp;rsquo;s &lt;em>not&lt;/em> public is private. This isn&amp;rsquo;t true for IPv4 (or
IPv6), but sending private data to a link-local or loopback address is probably(?) okay.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>Though nothing can stop the truly clueless developer. Someone could still write this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-perl" data-lang="perl">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nb">eval&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nn">IPv4&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$some_addr&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">is_public&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">send_private_data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$some_addr&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">send_public_data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$some_addr&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>But if you write code that intentionally ignores exceptions that &lt;em>you should not ignore&lt;/em> I give
up.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Security Issues in Perl IP Address distros</title><link>https://blog.urth.org/2021/03/29/security-issues-in-perl-ip-address-distros/</link><pubDate>Mon, 29 Mar 2021 13:33:39 -0500</pubDate><guid>https://blog.urth.org/2021/03/29/security-issues-in-perl-ip-address-distros/</guid><description>&lt;p>&lt;em>Edit on 2021-03-29 21:40(ish) UTC:&lt;/em> Added Net-Subnet (appears unaffected) and reordered the details
to match the list at the top of the post.&lt;/p>
&lt;p>&lt;em>Edit on 2021-03-30 14:50(ish) UTC:&lt;/em> Added Net-Works (appears unaffected).&lt;/p>
&lt;p>&lt;em>Edit on 2021-03-30 15:40(ish) UTC:&lt;/em> Added Net-CIDR (some functions are affected).&lt;/p>
&lt;p>&lt;em>Edit on 2021-03-31 01:05(ish) UTC:&lt;/em> Added Net-IPv4Addr (affected).&lt;/p>
&lt;p>&lt;em>Edit on 2021-04-05 01:21(ish) UTC:&lt;/em> Net-CIDR-Lite 0.22 contains a remediation.&lt;/p>
&lt;p>&lt;em>Edit on 2021-04-05 19:30(ish) UTC:&lt;/em> Net-IPAddress-Util 5.000 contains a remediation.&lt;/p>
&lt;p>&lt;em>Edit on 2025-06-01 23:30(ish) UTC:&lt;/em> Net-CIDR 0.25 contains a remediation.&lt;/p>
&lt;div class="shortcode-notice warning">
&lt;div class="shortcode-notice-title warning">
warning&lt;/div>
&lt;div class="notice-content">&lt;p>&lt;strong>TLDR: Some Perl modules for working with IP addresses and netmasks have
bugs with potential security applications.&lt;/strong> See below for more details on the bug and which modules
are affected.&lt;/p>
&lt;ul>
&lt;li>Net-IPv4Addr: Affected.&lt;/li>
&lt;li>Net-CIDR-Lite: Vulnerable before the 0.22 release. Upgrade now.&lt;/li>
&lt;li>Net-Netmask: Vulnerable before the 2.00000 release. Upgrade now.&lt;/li>
&lt;li>Net-IPAddress-Util: Vulnerable before the 5.000 release. Upgrade now.&lt;/li>
&lt;li>Data-Validate-IP: Depends on exactly how it&amp;rsquo;s used. See below for details.&lt;/li>
&lt;li>Net-CIDR: Depends on exactly how it&amp;rsquo;s used before the 0.25 release. Upgrade now.&lt;/li>
&lt;li>Socket: Appears unaffected.&lt;/li>
&lt;li>Net-DNS: Appears unaffected.&lt;/li>
&lt;li>NetAddr-IP: Appears unaffected.&lt;/li>
&lt;li>Net-Works: Appears unaffected.&lt;/li>
&lt;li>Net-Subnet: Appears unaffected.&lt;/li>
&lt;li>Net-Patricia: Appears unaffected.&lt;/li>
&lt;/ul>&lt;/div>
&lt;/div>
&lt;p>Yesterday,
&lt;a href="https://sick.codes/universal-netmask-npm-package-used-by-270000-projects-vulnerable-to-octal-input-data-server-side-request-forgery-remote-file-inclusion-local-file-inclusion-and-more-cve-2021-28918/">a security issue with the NPM package &lt;code>netmask&lt;/code> was published&lt;/a>&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>The issue itself is pretty straightforward. Your OS will allow an IP address like &lt;code>010.0.0.1&lt;/code> or a
netmask like &lt;code>010.0.0.0/8&lt;/code>. That &lt;code>010&lt;/code> is treated &lt;strong>as an octal number, not a base-10 number with a
leading zero!&lt;/strong> That means that &lt;code>010.0.0.1&lt;/code> is actually &lt;code>8.0.0.1&lt;/code>. We can confirm this with &lt;code>ping&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">ping 010.0.0.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PING 010.0.0.1 (8.0.0.1) 56(84) bytes of data.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>But the NPM &lt;code>netmask&lt;/code> package would treat this as &lt;code>10.0.0.1&lt;/code>. This confusion means that an
application could be tricked into thinking a public IP - &lt;code>8.0.0.1&lt;/code> - was part of a private subnet -
&lt;code>10.0.0.0/8&lt;/code>. And conversely, you could trick it into thinking a private IP - &lt;code>10.0.0.1&lt;/code> written as
&lt;code>012.0.0.1&lt;/code> - was part of a public subnet - &lt;code>12.0.0.1&lt;/code>.&lt;/p>
&lt;p>This has security implications for any application that is trying to distinguish between public and
private IP addresses or networks for access control, firewalling, etc.&lt;/p>
&lt;p>As I was reading about this I checked out
&lt;a href="https://github.com/rs/node-netmask">the Git repo for the &lt;code>netmask&lt;/code> package&lt;/a>. Its README says &amp;ldquo;This
module is highly inspired by Perl &lt;a href="http://search.cpan.org/dist/Net-Netmask/">Net::Netmask&lt;/a> module.&amp;rdquo;&lt;/p>
&lt;p>And at that point I realized that it was quite possible that this affected Perl code as well! So I
started digging into this by looking at various CPAN modules for working with IP addresses,
networks, and netmasks.&lt;/p>
&lt;p>Here&amp;rsquo;s the current state of CPAN modules, ordered roughly by their position in
&lt;a href="https://neilb.org/2015/04/20/river-of-cpan.html">The River of CPAN&lt;/a> (which basically means how many
modules depend on them).&lt;/p>
&lt;h2 id="net-ipv4addrhttpsmetacpanorgreleasenet-ipv4addr">&lt;a href="https://metacpan.org/release/Net-IPv4Addr">&lt;code>Net-IPv4Addr&lt;/code>&lt;/a>&lt;/h2>
&lt;div class="shortcode-notice warning">
&lt;div class="shortcode-notice-title warning">
warning&lt;/div>
&lt;div class="notice-content">&lt;strong>This distribution is affected by this issue. In addition, this module is
almost certainly no longer being maintained. Emails to the author bounce.&lt;/strong>&lt;/div>
&lt;/div>
&lt;p>This distribution has 4 direct dependents and 12 total dependents.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">perl -MNet::IPv4Addr=:all -E &amp;#39;say $_ for ipv4_network(&amp;#34;010.0.0.1&amp;#34;)&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">10.0.0.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">8
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="net-cidr-litehttpsmetacpanorgreleasenet-cidr-lite">&lt;a href="https://metacpan.org/release/Net-CIDR-Lite">&lt;code>Net-CIDR-Lite&lt;/code>&lt;/a>&lt;/h2>
&lt;div class="shortcode-notice info">
&lt;div class="shortcode-notice-title info">
info&lt;/div>
&lt;div class="notice-content">&lt;strong>This distribution was vulnerable prior to its 0.22 release made on 2021-04-04.
Thanks to Stig Palmquist for taking this distro over and releasing a fix!&lt;/strong>&lt;/div>
&lt;/div>
&lt;p>This distribution has 24 direct dependents and 36 total dependents.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">perl -MNet::CIDR::Lite -E &amp;#39;my $c = Net::CIDR::Lite-&amp;gt;new; $c-&amp;gt;add(&amp;#34;010.0.0.0/8&amp;#34;); say $_ for $c-&amp;gt;list_range&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Can&amp;#39;t determine ip format at /home/autarch/.perlbrew/libs/perl-5.30.1@dev/lib/perl5/Net/CIDR/Lite.pm line 38.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Net::CIDR::Lite::add(Net::CIDR::Lite=HASH(0x55fe55ade740), &amp;#34;010.0.0.0/8&amp;#34;) called at -e line 1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="net-netmaskhttpsmetacpanorgreleasenet-netmask">&lt;a href="https://metacpan.org/release/Net-Netmask">&lt;code>Net-Netmask&lt;/code>&lt;/a>&lt;/h2>
&lt;div class="shortcode-notice info">
&lt;div class="shortcode-notice-title info">
info&lt;/div>
&lt;div class="notice-content">&lt;strong>This distribution was vulnerable prior to its 2.0000 release earlier today.
Great job on the quick response, Joelle Maslak!&lt;/strong>&lt;/div>
&lt;/div>
&lt;p>This distribution has 22 direct dependents and 30 total dependents.&lt;/p>
&lt;p>So for versions before 2.0000 we see this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">perl -MNet::Netmask -E &amp;#39;say defined Net::Netmask-&amp;gt;new2(q{010.0.0.0/8}) ? 1 : 0&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Note the use of the &lt;code>new2&lt;/code> constructor. The old &lt;code>new&lt;/code> constructor cannot be changed to return
&lt;code>undef&lt;/code> for backwards compatibility reasons. Fortunately, it&amp;rsquo;s probably not vulnerable in any
exploitable way, as it returns a 0-length subnet:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">perl -MNet::Netmask -E &amp;#39;say Net::Netmask-&amp;gt;new(q{010.0.0.0/8})&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">0.0.0.0/0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="net-ipaddress-utilhttpsmetacpanorgreleasenet-ipaddress-util">&lt;a href="https://metacpan.org/release/Net-IPAddress-Util">&lt;code>Net-IPAddress-Util&lt;/code>&lt;/a>&lt;/h2>
&lt;div class="shortcode-notice info">
&lt;div class="shortcode-notice-title info">
info&lt;/div>
&lt;div class="notice-content">&lt;strong>This distribution was vulnerable prior to its 5.000 release made on
2021-04-04. Thanks to Paul W Bennett for the fix!&lt;/strong>&lt;/div>
&lt;/div>
&lt;p>This distribution has no dependents.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">perl -MNet::IPAddress::Util=IP -E &amp;#39;say IP(q{010.0.0.1})&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">8.0.0.1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="data-validate-iphttpsmetacpanorgreleasedata-validate-ip">&lt;a href="https://metacpan.org/release/Data-Validate-IP">&lt;code>Data-Validate-IP&lt;/code>&lt;/a>&lt;/h2>
&lt;div class="shortcode-notice info">
&lt;div class="shortcode-notice-title info">
info&lt;/div>
&lt;div class="notice-content">&lt;strong>This distribution doesn&amp;rsquo;t misparse octal numbers, but you could be affected
depending on exactly how your code uses this distro. See below for details.&lt;/strong>&lt;/div>
&lt;/div>
&lt;p>This distribution has 21 direct dependents and 60 total dependents.&lt;/p>
&lt;p>This distribution returns false for any &lt;code>is_*_ipv4&lt;/code> method that includes an octal number. So both
&lt;code>is_private_ipv4('010.0.0.1')&lt;/code> and &lt;code>is_public_ipv4('010.0.0.1')&lt;/code> return false. &lt;strong>Depending on how
you&amp;rsquo;re using this module, it&amp;rsquo;s &lt;em>possible&lt;/em> that this could lead to bugs, including bugs with security
implications.&lt;/strong>&lt;/p>
&lt;p>I
&lt;a href="https://metacpan.org/pod/Data::Validate::IP#USAGE-AND-SECURITY-RECOMMENDATIONS">updated the documentation&lt;/a>
to explicitly recommend that you &lt;strong>always call &lt;code>is_ipv4()&lt;/code> in addition to calling a method like
&lt;code>is_private_ipv4()&lt;/code>&lt;/strong>. The &lt;code>is_ipv4()&lt;/code> method will always return false for IP addresses with octal
numbers.&lt;/p>
&lt;p>While this isn&amp;rsquo;t strictly POSIX-correct, this seems like the safest behavior for a module like this.
It&amp;rsquo;s better to be too strict if this eliminates a potential footgun.&lt;/p>
&lt;p>&lt;strong>If you are using this distribution, I highly encourage you to audit your use of it in a security
context!&lt;/strong>&lt;/p>
&lt;h2 id="net-cidrhttpsmetacpanorgreleasenet-cidr">&lt;a href="https://metacpan.org/release/Net-CIDR">&lt;code>Net-CIDR&lt;/code>&lt;/a>&lt;/h2>
&lt;div class="shortcode-notice info">
&lt;div class="shortcode-notice-title info">
info&lt;/div>
&lt;div class="notice-content">&lt;strong>This distribution was vulnerable prior to its 0.25 release made on
2025-05-23.&lt;/strong>&lt;/div>
&lt;/div>
&lt;p>This distribution has 17 direct dependents and 25 total dependents.&lt;/p>
&lt;p>The distribution provides a number of functions for working with networks and IP addresses. Most of
these are not affected. However, two are:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">perl -MNet::CIDR -E &amp;#39;say for Net::CIDR::addr2cidr(&amp;#34;010.0.0.1&amp;#34;)&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">010.0.0.1/32
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">010.0.0.0/31
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">010.0.0.0/8
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">10.0.0.0/7
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">8.0.0.0/6
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -MNet::CIDR -E &amp;#39;say Net::CIDR::cidrlookup(&amp;#34;10.0.0.1&amp;#34;, &amp;#34;010.0.0.0/8&amp;#34;)&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>However, this distribution also contains a &lt;code>cidrvalidate&lt;/code> function that will return false for any
CIDR string with a leading 0 in an octet. The documentation explicitly tells you to use this before
passing the data to other functions.&lt;/p>
&lt;p>&lt;strong>If you are using this distribution, I highly encourage you to audit your use of it in a security
context!&lt;/strong>&lt;/p>
&lt;h2 id="sockethttpsmetacpanorgreleasesocket">&lt;a href="https://metacpan.org/release/Socket">&lt;code>Socket&lt;/code>&lt;/a>&lt;/h2>
&lt;div class="shortcode-notice note">
&lt;div class="shortcode-notice-title note">
note&lt;/div>
&lt;div class="notice-content">&lt;strong>This distribution appears to be unaffected by this issue.&lt;/strong>&lt;/div>
&lt;/div>
&lt;p>This distribution has 275 direct dependents and 9,936 total dependents.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">perl -MSocket -E &amp;#39;say inet_ntoa(inet_aton(q{010.0.0.1}))&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">8.0.0.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -MSocket=inet_pton,inet_ntop,AF_INET -E &amp;#39;say inet_ntop(AF_INET, inet_pton(AF_INET, q{010.0.0.1}))&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Bad address length for Socket::inet_ntop on AF_INET; got 0, should be 4 at -e line 1.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The &lt;code>inet_pton()&lt;/code> function is just returning &lt;code>undef&lt;/code> for this octal-formatted address.&lt;/p>
&lt;h2 id="net-dnshttpsmetacpanorgreleasenet-dns">&lt;a href="https://metacpan.org/release/Net-DNS">&lt;code>Net-DNS&lt;/code>&lt;/a>&lt;/h2>
&lt;div class="shortcode-notice note">
&lt;div class="shortcode-notice-title note">
note&lt;/div>
&lt;div class="notice-content">&lt;strong>This distribution appears to be unaffected by this issue.&lt;/strong>&lt;/div>
&lt;/div>
&lt;p>This distribution has 104 direct dependents and 561 total dependents.&lt;/p>
&lt;p>If you try to resolve an IP address, it turns this into a reverse lookup, but it treats the IP as
text:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">perl ./demo/perldig 010.0.0.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">;; Response received from 127.0.0.53 (40 octets)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">;; HEADER SECTION
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">;; id = 6342
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">;; qr = 1 aa = 0 tc = 0 rd = 1 opcode = QUERY
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">;; ra = 1 z = 0 ad = 0 cd = 0 rcode = NXDOMAIN
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">;; qdcount = 1 ancount = 0 nscount = 0 arcount = 0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">;; do = 0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">;; QUESTION SECTION (1 record)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">;; 1.0.0.010.in-addr.arpa. IN A
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>So it&amp;rsquo;s not a useful answer, but it&amp;rsquo;s not looking up the &lt;em>wrong&lt;/em> address.&lt;/p>
&lt;h2 id="netaddr-iphttpsmetacpanorgreleasenetaddr-ip">&lt;a href="https://metacpan.org/release/NetAddr-IP">&lt;code>NetAddr-IP&lt;/code>&lt;/a>&lt;/h2>
&lt;div class="shortcode-notice note">
&lt;div class="shortcode-notice-title note">
note&lt;/div>
&lt;div class="notice-content">&lt;strong>This distribution appears to be unaffected by this issue.&lt;/strong>&lt;/div>
&lt;/div>
&lt;p>This distribution has 36 direct dependents and 110 total dependents.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">perl -MNetAddr::IP -E &amp;#39;say NetAddr::IP-&amp;gt;new(q{010.0.0.024})&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">8.0.0.20/32
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="net-workshttpsmetacpanorgreleasenet-works">&lt;a href="https://metacpan.org/release/Net-Works">&lt;code>Net-Works&lt;/code>&lt;/a>&lt;/h2>
&lt;div class="shortcode-notice note">
&lt;div class="shortcode-notice-title note">
note&lt;/div>
&lt;div class="notice-content">&lt;strong>This distribution appears to be unaffected by this issue.&lt;/strong>&lt;/div>
&lt;/div>
&lt;p>This distribution has 3 direct dependents and 7 total dependents.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">perl -MNet::Works::Network -E &amp;#39;say Net::Works::Network-&amp;gt;new_from_string(string =&amp;gt; q{010.0.0.1/8})&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">010.0.0.1/8 is not a valid IP network at /home/autarch/.perlbrew/libs/perl-5.30.1@dev/lib/perl5/Net/Works/Network.pm line 120.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Net::Works::Network::new_from_string(&amp;#34;Net::Works::Network&amp;#34;, &amp;#34;string&amp;#34;, &amp;#34;010.0.0.1/8&amp;#34;) called at -e line 1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -MNet::Works::Address -E &amp;#39;say Net::Works::Address-&amp;gt;new_from_string(string =&amp;gt; q{010.0.0.1})&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">010.0.0.1 is not a valid IPv6 address at /home/autarch/.perlbrew/libs/perl-5.30.1@dev/lib/perl5/Net/Works/Util.pm line 70.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Net::Works::Util::_validate_ip_string(&amp;#34;010.0.0.1&amp;#34;, 6) called at /home/autarch/.perlbrew/libs/perl-5.30.1@dev/lib/perl5/Net/Works/Address.pm line 74
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Net::Works::Address::new_from_string(&amp;#34;Net::Works::Address&amp;#34;, &amp;#34;string&amp;#34;, &amp;#34;010.0.0.1&amp;#34;) called at -e line 1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Thanks to Stig Palmquist for checking this one and letting me know.&lt;/p>
&lt;h2 id="net-subnethttpsmetacpanorgreleasenet-subnet">&lt;a href="https://metacpan.org/release/Net-Subnet">&lt;code>Net-Subnet&lt;/code>&lt;/a>&lt;/h2>
&lt;div class="shortcode-notice note">
&lt;div class="shortcode-notice-title note">
note&lt;/div>
&lt;div class="notice-content">&lt;strong>This distribution appears to be unaffected by this issue.&lt;/strong>&lt;/div>
&lt;/div>
&lt;p>This distribution has 3 direct dependents and 7 total dependents.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">perl -MNet::Subnet -E &amp;#39;my $m = subnet_matcher(q{10.0.0.0/8}); say $m-&amp;gt;(q{012.0.0.1}) ? 1 : 0&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -MNet::Subnet -E &amp;#39;my $m = subnet_matcher(q{012.0.0.0/8}); say $m-&amp;gt;(q{10.0.0.1}) ? 1 : 0&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="net-patriciahttpsmetacpanorgreleasenet-patricia">&lt;a href="https://metacpan.org/release/Net-Patricia">&lt;code>Net-Patricia&lt;/code>&lt;/a>&lt;/h2>
&lt;div class="shortcode-notice note">
&lt;div class="shortcode-notice-title note">
note&lt;/div>
&lt;div class="notice-content">&lt;strong>This distribution appears to be unaffected by this issue.&lt;/strong>&lt;/div>
&lt;/div>
&lt;p>This distribution has 1 direct dependent and 1 total dependent.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">perl -MNet::Patricia -E &amp;#39;my $p = Net::Patricia-&amp;gt;new; $p-&amp;gt;add_string(&amp;#34;010.0.0.0/8&amp;#34;); say $p-&amp;gt;match_string(&amp;#34;8.0.0.1&amp;#34;) ? 1 : 0&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">perl -MNet::Patricia -E &amp;#39;my $p = Net::Patricia-&amp;gt;new; $p-&amp;gt;add_string(&amp;#34;8.0.0.0/8&amp;#34;); say $p-&amp;gt;match_string(&amp;#34;010.0.0.1&amp;#34;) ? 1 : 0&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>One of the most ridiculous URL paths I&amp;rsquo;ve ever seen.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Down the Golang nil Rabbit Hole</title><link>https://blog.urth.org/2021/03/27/down-the-golang-nil-rabbit-hole/</link><pubDate>Sat, 27 Mar 2021 14:36:28 -0500</pubDate><guid>https://blog.urth.org/2021/03/27/down-the-golang-nil-rabbit-hole/</guid><description>&lt;p>&lt;em>Edit 2021-03-30&lt;/em>: Jeremy Mikkola
&lt;a href="http://jeremymikkola.com/posts/2017_03_29_know_your_nil.html">wrote about some closely related topics&lt;/a>
back in 2017.&lt;/p>
&lt;p>&lt;em>Edit 2021-03-31&lt;/em>: Chris Siebenmann
&lt;a href="https://utcc.utoronto.ca/~cks/space/blog/programming/GoNilIsTypedSortOf">wrote a response to this post&lt;/a>
that explains exactly how interface values that are &lt;code>nil&lt;/code> are typed. It&amp;rsquo;s more complicated than I
thought!&lt;/p>
&lt;p>I&amp;rsquo;m not sure I have another Rust &amp;amp; Postgres blog post in me right now, so let&amp;rsquo;s learn something
about Go instead.&lt;/p>
&lt;p>Recently I decided I wanted to add a &lt;code>--unique&lt;/code> flag to
&lt;a href="https://github.com/houseabsolute/omegasort">omegasort&lt;/a>. Wait, what&amp;rsquo;s omegasort?&lt;/p>
&lt;p>It&amp;rsquo;s a text file sorting tool that supports lots of different sorting methods. For example, in
addition a standard text sort, it can sort numbered lines, date-prefixed lines, paths (including
Windows paths with and without drive letters), IP addresses, and IP networks. It also supports
Unicode locales, reverse sorting, and locale-aware case insensitive sorting.&lt;/p>
&lt;p>I use it together with &lt;a href="https://github.com/houseabsolute/precious">precious&lt;/a> to sort things like
&lt;code>.gitignore&lt;/code> files, spellchecker allowlists, and things of that nature.&lt;/p>
&lt;p>I realized that I really wanted a &lt;code>--unique&lt;/code> flag for all of this. While I could just pipe its
output to &lt;code>uniq&lt;/code> on a *nix system, this doesn&amp;rsquo;t work so well on Windows. Plus with tools like
precious it&amp;rsquo;s easier if I can use one binary for a given task. If I want to pipe things I have to
put that in a shell script that precious calls.&lt;/p>
&lt;p>But my rabbit hole experience didn&amp;rsquo;t happen with omegasort directly. Instead, it happened when I
tried to add some integration tests.&lt;/p>
&lt;p>While writing those integration tests, I was using
&lt;a href="https://github.com/houseabsolute/detest">&lt;code>github.com/houseabsolute/detest&lt;/code>&lt;/a>. This is a Golang
package I created that offers a test assertion interface inspired by
&lt;a href="https://metacpan.org/release/Test2-Suite">&lt;code>Test2-Suite&lt;/code>&lt;/a> in Perl.&lt;/p>
&lt;p>For reference, here&amp;rsquo;s a &lt;code>Test2-Suite&lt;/code> example:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-perl" data-lang="perl">&lt;span class="line">&lt;span class="cl">&lt;span class="k">use&lt;/span> &lt;span class="nn">Test2::Suite&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">object&lt;/span> &lt;span class="n">Subtest&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="k">sub&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">call&lt;/span> &lt;span class="n">name&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="s">&amp;#39;TestsFor::Basic&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">call&lt;/span> &lt;span class="n">pass&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">T&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">call&lt;/span> &lt;span class="n">subevents&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">array&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">object&lt;/span> &lt;span class="n">Plan&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="k">sub&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">call&lt;/span> &lt;span class="n">max&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">call&lt;/span> &lt;span class="n">trace&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">object&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">call&lt;/span> &lt;span class="k">package&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="s">&amp;#39;Test::Class::Moose::Role::Executor&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">call&lt;/span> &lt;span class="n">subname&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="s">&amp;#39;Test::Class::Moose::Util::context_do&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I think this is pretty self-explanatory, except for &lt;code>T()&lt;/code>, which means &amp;ldquo;true&amp;rdquo;.&lt;/p>
&lt;p>And here&amp;rsquo;s something like that in Go with &lt;code>detest&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;testing&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;github.com/houseabsolute/detest/pkg/detest&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">TestSomething&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">t&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">testing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">T&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">d&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">detest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">New&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">t&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Is&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">someStruct&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Struct&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">st&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">detest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">StructTester&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Field&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;size&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">43&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Field&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;Douglas&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Map&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">mt&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">detest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">MapTester&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">mt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Key&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;foo&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Slice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">st&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">detest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SliceTester&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Idx&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Map&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">mt&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">detest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">MapTester&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">mt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Key&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;bar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Slice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">st&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">detest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SliceTester&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Idx&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;buz&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Idx&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;not quux&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Idx&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Map&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">mt&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">detest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">MapTester&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">mt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Key&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;nosuchkey&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Slice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">st&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">detest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SliceTester&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Idx&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;buz&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Idx&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;not quux&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>It&amp;rsquo;s not as nice as the Perl version because it gets quite verbose, but this was the closest I could
come. Go&amp;rsquo;s type system, combined with a lack of syntactic flexibility, means a whole lot of func
calls, braces, and parens.&lt;/p>
&lt;p>Under the hood, this is implemented with a metric fork ton of runtime reflection using the stdlib&amp;rsquo;s
&lt;a href="https://pkg.go.dev/reflect">&lt;code>reflect&lt;/code> package&lt;/a>. I don&amp;rsquo;t love this, but absent generics, there&amp;rsquo;s no
other way to implement this sort of API except with code generation. And that codegen would have to
be fed by a sort-of-Go language that was translated to real Go, which seems like a terrible idea.&lt;/p>
&lt;h2 id="getting-to-the-darn-point">Getting to the Darn Point&lt;/h2>
&lt;p>So while I was writing those omegasort integration tests using detest, I managed to find a whole lot
of bugs in detest.&lt;/p>
&lt;p>But the title says &lt;code>nil&lt;/code> and I haven&amp;rsquo;t mentioned those yet.&lt;/p>
&lt;p>So here&amp;rsquo;s a fun fact, Go has multiple &amp;ldquo;types&amp;rdquo; of &lt;code>nil&lt;/code>. Specifically, there are both typed and
untyped &lt;code>nil&lt;/code> variables. This surprised me at first, but it makes sense when you think about it.&lt;/p>
&lt;p>Let&amp;rsquo;s take this code&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">package&lt;/span> &lt;span class="nx">main&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;fmt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;reflect&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">v1&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">reflect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ValueOf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">nil&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">uninit&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">int&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">v2&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">reflect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ValueOf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">uninit&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">logValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;nil&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">v1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">logValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;[]int&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">v2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;[]int == nil? %v\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">uninit&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">logValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">what&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">v&lt;/span> &lt;span class="nx">reflect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Value&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%s is valid? %v\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">what&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">IsValid&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">IsValid&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%s is nil? %v\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">what&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">IsNil&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%s type = %v\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">what&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Type&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This prints out the following:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">nil is valid? false
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[]int is valid? true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[]int is nil? true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[]int type = []int
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[]int == nil? true
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>So a bare &lt;code>nil&lt;/code> and a variable that has a type but no value are equal, but if you try to get a
&lt;code>reflect.Value&lt;/code> for &lt;code>nil&lt;/code>, it&amp;rsquo;s not valid. If you try to call other methods like &lt;code>v.IsNil()&lt;/code> or
&lt;code>v.Type()&lt;/code> on an invalid&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> &lt;code>reflect.Value&lt;/code>, you will get a panic.&lt;/p>
&lt;p>I encountered this when trying to test that an &lt;code>error&lt;/code> returned by a func call was &lt;code>nil&lt;/code>.&lt;/p>
&lt;p>This led to a flurry of &lt;a href="https://github.com/houseabsolute/detest/releases">&lt;code>detest&lt;/code> releases&lt;/a> as I
realized how many parts of the &lt;code>detest&lt;/code> code this impacted. In most places where it uses &lt;code>reflect&lt;/code>,
I have to guard against a bare &lt;code>nil&lt;/code> being passed in.&lt;/p>
&lt;p>But wait, it gets even more confusing. Sometimes the Go compiler will turn an untyped &lt;code>nil&lt;/code> into a
typed &lt;code>nil&lt;/code>. Here&amp;rsquo;s an example&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">package&lt;/span> &lt;span class="nx">main&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;fmt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;reflect&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">takesSlice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;nil&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">uninit&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">int&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">takesSlice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;[]int&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">uninit&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">takesSlice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">what&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">s&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">logValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">what&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">reflect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ValueOf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">s&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">logValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">what&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">v&lt;/span> &lt;span class="nx">reflect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Value&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%s is valid? %v\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">what&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">IsValid&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">IsValid&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%s is nil? %v\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">what&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">IsNil&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%s type = %v\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">what&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Type&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And when we run it we get this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">nil is valid? true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">nil is nil? true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">nil type = []int
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[]int is valid? true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[]int is nil? true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[]int type = []int
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>So when I pass a bare &lt;code>nil&lt;/code> to &lt;code>takesSlice&lt;/code>, it gets typed as whatever type the function&amp;rsquo;s signature
says it should be.&lt;/p>
&lt;p>But wait, it gets even more confusing yet again! Sometimes the Go compiler &lt;strong>won&amp;rsquo;t&lt;/strong> turn an untyped
&lt;code>nil&lt;/code> into a typed &lt;code>nil&lt;/code>. Here&amp;rsquo;s an example&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">package&lt;/span> &lt;span class="nx">main&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;fmt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;reflect&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">takesError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;nil&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">uninit&lt;/span> &lt;span class="kt">error&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">takesError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;error&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">uninit&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">takesError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">what&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">logValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">what&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">reflect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ValueOf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">logValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">what&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">v&lt;/span> &lt;span class="nx">reflect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Value&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%s is valid? %v\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">what&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">IsValid&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">IsValid&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%s is nil? %v\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">what&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">IsNil&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%s type = %v\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">what&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Type&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If the type of the argument in the function signature is any type of interface, including
&lt;code>interface{}&lt;/code>, then the underlying value is still untyped and not valid. This &amp;hellip; sort of makes
sense? I think the way this works is that anything typed as an interface also has a &lt;em>real&lt;/em>
underlying type. So an &lt;code>error&lt;/code> can be an &lt;code>errors.errorString&lt;/code> or an &lt;code>exec.ExitError&lt;/code> or a
&lt;code>mypackage.DogError&lt;/code>. But if we pass a bare &lt;code>nil&lt;/code> or an uninitialized variable, there&amp;rsquo;s no
underlying type.&lt;/p>
&lt;p>This came up with detest when I wanted to test that I &lt;em>didn&amp;rsquo;t&lt;/em> get an error from a call.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">doThing&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Is&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;no error from doing a thing&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Under the hood, the signature for &lt;code>d.Is()&lt;/code> uses &lt;code>interface{}&lt;/code> for the two arguments being compared.
So bare &lt;code>nil&lt;/code> as the second argument will &lt;em>never&lt;/em> be valid. And the first argument might be valid or
it might not be. If &lt;code>doThing()&lt;/code>&amp;rsquo;s return type is just &lt;code>error&lt;/code> and it returns a &lt;code>nil&lt;/code>, then the value
in &lt;code>err&lt;/code> has no type.&lt;/p>
&lt;p>All of this led to a fair bit more code in the &lt;code>detest&lt;/code> guts to handle this. For example, just
because two variables don&amp;rsquo;t have the same type doesn&amp;rsquo;t mean they&amp;rsquo;re not equal (from Go&amp;rsquo;s
perspective). A bare &lt;code>nil&lt;/code> and an uninitialized slice are equal when compared with &lt;code>==&lt;/code>, which is
what &lt;code>d.Is()&lt;/code> emulates using &lt;code>reflect&lt;/code>.&lt;/p>
&lt;p>So there&amp;rsquo;s quite a few cases around one or both arguments being invalid that need handling. And
there are MANY other methods with the same issues to consider, including things like &lt;code>d.Map()&lt;/code> and
&lt;code>d.Struct()&lt;/code>, all of which should handle an invalid value properly.&lt;/p>
&lt;h2 id="what-does-this-look-like-in-other-languages">What Does This Look Like in Other Languages?&lt;/h2>
&lt;p>Well, I don&amp;rsquo;t know &lt;em>that&lt;/em> many other languages. In Perl this isn&amp;rsquo;t really a thing, because it has a
pretty minimal type system. Perl&amp;rsquo;s &lt;code>undef&lt;/code> can be coerced to lots of things, although under
&lt;a href="https://perldoc.perl.org/strict">strict&lt;/a> trying to use an &lt;code>undef&lt;/code> in certain ways is an error, like
writing this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-perl" data-lang="perl">&lt;span class="line">&lt;span class="cl">&lt;span class="k">my&lt;/span> &lt;span class="nv">$x&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">say&lt;/span> &lt;span class="nv">@&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nv">$x&lt;/span>&lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This will blow up with &lt;code>Can't use an undefined value as an ARRAY reference ...&lt;/code> at line 2.&lt;/p>
&lt;p>Rust (at least safe Rust&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup>) doesn&amp;rsquo;t have any notion of &lt;code>nil&lt;/code> or undefined values. Instead, you
have the &lt;a href="https://doc.rust-lang.org/std/option/enum.Option.html">&lt;code>Option&amp;lt;T&amp;gt;&lt;/code>&lt;/a> type, which always has
a type. For example&lt;sup id="fnref:6">&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref">6&lt;/a>&lt;/sup>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">fn&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">String&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">b&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="kt">i32&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">None&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">println!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;a == b? &lt;/span>&lt;span class="si">{}&lt;/span>&lt;span class="s">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">==&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">b&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This just won&amp;rsquo;t compile. While both &lt;code>a&lt;/code> and &lt;code>b&lt;/code> are &lt;code>None&lt;/code>, they&amp;rsquo;re not the &lt;em>same type&lt;/em> of &lt;code>None&lt;/code> so
you can&amp;rsquo;t just compare them with &lt;code>==&lt;/code>. The compiler says:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-mysql" data-lang="mysql">&lt;span class="line">&lt;span class="cl">&lt;span class="n">error&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">E0308&lt;/span>&lt;span class="p">]:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">mismatched&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">types&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">--&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">src&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">main&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">rs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">33&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">println&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;a == b? {}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">==&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">b&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">^&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">expected&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">struct&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">found&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">i32&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">note&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">expected&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">enum&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="k">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">&amp;gt;`&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">found&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">enum&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="k">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">i32&lt;/span>&lt;span class="o">&amp;gt;`&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>By the way, aren&amp;rsquo;t these Rust compiler errors nice? The only other language I&amp;rsquo;ve seen with this type
of extremely detailed compiler errors is &lt;a href="https://raku.org/">Raku&lt;/a>.&lt;/p>
&lt;h2 id="in-summary">In Summary&lt;/h2>
&lt;p>It&amp;rsquo;s tempting to pick on Go and complain about it. I certainly do that a lot at work. But to be
fair, this really isn&amp;rsquo;t an issue for most Go code. It&amp;rsquo;s only because I&amp;rsquo;m trying to do weird stuff
with &lt;code>reflect&lt;/code> that I&amp;rsquo;m learning about this internal weirdness. In day to day Go code, the
compiler&amp;rsquo;s handling of various types of &lt;code>nil&lt;/code> &amp;ldquo;just works&amp;rdquo; the way you&amp;rsquo;d expect it to. And being
able to use a bare &lt;code>nil&lt;/code> is quite handy.&lt;/p>
&lt;p>But I still prefer how Rust does it, using a parameterized &lt;code>Option&amp;lt;T&amp;gt;&lt;/code> type. That way I can easily
check if something is &lt;code>None&lt;/code> without any special cases. Everything is using the same type system,
though that type system is much more complex than Go&amp;rsquo;s.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>&lt;a href="https://play.golang.org/p/Xo5hXUIw01U">https://play.golang.org/p/Xo5hXUIw01U&lt;/a>&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Note that an &amp;ldquo;invalid&amp;rdquo; value in the context of &lt;code>reflect&lt;/code> is not invalid in the context of a Go
program. You can use an invalid value everywhere you can use the corresponding valid but
uninitialized &lt;code>nil&lt;/code> value.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>&lt;a href="https://play.golang.org/p/HKQBiFCNINk">https://play.golang.org/p/HKQBiFCNINk&lt;/a>&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>&lt;a href="https://play.golang.org/p/NMsi05CH8r3">https://play.golang.org/p/NMsi05CH8r3&lt;/a>&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>I know very little about unsafe Rust which is why I&amp;rsquo;m hedging.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:6">
&lt;p>&lt;a href="https://play.rust-lang.org/?version=stable&amp;mode=debug&amp;edition=2018&amp;gist=677599a2ff660f57b51a31219f428312">https://play.rust-lang.org/?version=stable&amp;amp;mode=debug&amp;amp;edition=2018&amp;amp;gist=677599a2ff660f57b51a31219f428312&lt;/a>&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Writing a Postgres SQL Pretty Printer in Rust: Part 1.5</title><link>https://blog.urth.org/2021/03/21/writing-a-postgres-sql-pretty-printer-in-rust-part-1-5/</link><pubDate>Sun, 21 Mar 2021 13:19:05 -0500</pubDate><guid>https://blog.urth.org/2021/03/21/writing-a-postgres-sql-pretty-printer-in-rust-part-1-5/</guid><description>&lt;p>Last week I wrote the &lt;a href="https://blog.urth.org/2021/03/14/writing-a-postgres-sql-pretty-printer-in-rust-part-1/">first post in this series&lt;/a>, where I introduced the
project and wrote about generating Rust code for the parsed Postgres AST.&lt;/p>
&lt;p>I also wrote about the need for wrapper enums in the generated code, but I don&amp;rsquo;t think I went into
enough detail, based on questions and discussions I had after I
&lt;a href="https://www.reddit.com/r/rust/comments/m51oet/writing_a_postgres_sql_pretty_printer_in_rust/">shared that post in /r/rust&lt;/a>.&lt;/p>
&lt;p>So this week I will go into more detail on exactly why I had to do this.&lt;/p>
&lt;h2 id="series-links">Series Links&lt;/h2>
&lt;ul>
&lt;li>Part 1: &lt;a href="https://blog.urth.org/2021/03/14/writing-a-postgres-sql-pretty-printer-in-rust-part-1/">Introduction to the project and generating Rust with Perl&lt;/a>&lt;/li>
&lt;li>Part 1.5: &lt;strong>More about enum wrappers and serde&amp;rsquo;s externally tagged enum representation&lt;/strong>&lt;/li>
&lt;li>Part 2: &lt;a href="https://blog.urth.org/2021/04/24/writing-a-postgres-sql-pretty-printer-in-rust-part-2/">How I&amp;rsquo;m testing the pretty printer and how I generate tests from the Postgres
docs&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="a-tagged-enum-example">A Tagged Enum Example&lt;/h2>
&lt;p>I&amp;rsquo;ve made &lt;a href="https://github.com/autarch/tagged-enum-example">an example crate&lt;/a> with all of the code I
walk through below at &lt;a href="https://github.com/autarch/tagged-enum-example">https://github.com/autarch/tagged-enum-example&lt;/a>.&lt;/p>
&lt;p>In order to make this simpler, I&amp;rsquo;ll use some very simple JSON, as opposed to the rather complex JSON
we get back from the Pg parser. However, I cannot change the JSON to make parsing easier, just like
I cannot do that with the Pg parser&amp;rsquo;s output&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;Root&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;first&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;Foo&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;size&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">42&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;blue&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;second&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;Bar&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mood&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;indigo&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;car&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Super&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;actions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;Run&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;speed&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">84&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;Sleep&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;hours&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">8&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I&amp;rsquo;ll use &lt;a href="https://tools.ietf.org/html/draft-goessner-dispatch-jsonpath-00">JSONPath&lt;/a> to refer to
parts of the document. You can see that every object in the JSON is &amp;ldquo;tagged&amp;rdquo; with its type. Those
are the title case keys: &lt;code>$.Root&lt;/code>, &lt;code>$.Root.first.Foo&lt;/code>, &lt;code>$.Root.second.Bar&lt;/code>, &lt;code>$.Root.actions[0].Run&lt;/code>,
and &lt;code>$.Root.actions[1].Sleep&lt;/code>.&lt;/p>
&lt;p>Let&amp;rsquo;s assume that the &lt;code>$.Root.second&lt;/code> key is optional, so it could be entirely omitted in some
documents.&lt;/p>
&lt;h3 id="the-naive-approach">The Naive Approach&lt;/h3>
&lt;p>Now let&amp;rsquo;s make some Rust structs that correspond to this JSON. This corresponds to the
&lt;a href="https://github.com/autarch/tagged-enum-example/tree/master/naive">naive directory&lt;/a> in my example
repo.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="cp">#[derive(Debug, Deserialize)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">struct&lt;/span> &lt;span class="nc">Root&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">first&lt;/span>: &lt;span class="nc">Foo&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">second&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Bar&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">actions&lt;/span>: &lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Action&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="cp">#[derive(Debug, Deserialize)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">struct&lt;/span> &lt;span class="nc">Foo&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">size&lt;/span>: &lt;span class="kt">i8&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">color&lt;/span>: &lt;span class="nb">String&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="cp">#[derive(Debug, Deserialize)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">struct&lt;/span> &lt;span class="nc">Bar&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">mood&lt;/span>: &lt;span class="nb">String&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">car&lt;/span>: &lt;span class="nb">String&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="cp">#[derive(Debug, Deserialize)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">enum&lt;/span> &lt;span class="nc">Action&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Run&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">speed&lt;/span>: &lt;span class="kt">i64&lt;/span> &lt;span class="p">},&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Sleep&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">hours&lt;/span>: &lt;span class="kt">i8&lt;/span> &lt;span class="p">},&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This is all pretty straightforward. We have a &lt;code>Root&lt;/code> struct that can contain a &lt;code>Foo&lt;/code>, an optional
&lt;code>Bar&lt;/code>, and zero or more &lt;code>Action&lt;/code> structs.&lt;/p>
&lt;p>And here&amp;rsquo;s our parsing code:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fn&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">output&lt;/span>: &lt;span class="nc">Root&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">serde_json&lt;/span>::&lt;span class="n">from_str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="no">DOC&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="n">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;parsed&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">println!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;&lt;/span>&lt;span class="si">{:#?}&lt;/span>&lt;span class="s">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">output&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>So what happens when we run this?&lt;/p>
&lt;p>We get this error:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-mysql" data-lang="mysql">&lt;span class="line">&lt;span class="cl">&lt;span class="p">...&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;parsed: Error(&amp;#34;missing field `first`&amp;#34;, line: 29, column: 1)&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The important bit is &lt;code>&amp;quot;missing field `first`&amp;quot;, line: 29, column: 1&lt;/code>. What&amp;rsquo;s at line 29, column 1
of our JSON document? That&amp;rsquo;s the end of the document, actually.&lt;/p>
&lt;p>So basically we&amp;rsquo;re seeing that the serde JSON parser looked through the entire top-level object for
a &lt;code>first&lt;/code> key but could not find one. That makes sense, since the top-level object in the actual
document only contains a key named &lt;code>Root&lt;/code>.&lt;/p>
&lt;p>Fortunately, serde has a solution to this, in the form of its
&lt;a href="https://serde.rs/enum-representations.html#externally-tagged">&amp;ldquo;externally tagged enum representation&amp;rdquo;&lt;/a>
handling. For this type of JSON, each object is annotated with an extra &amp;ldquo;tag&amp;rdquo; indicating its type,
just like we see with &lt;code>$.Root&lt;/code> and &lt;code>$.Root.first.Foo&lt;/code> and so on.&lt;/p>
&lt;p>But the key word here is &amp;ldquo;enum&amp;rdquo;. Serde does not offer a way to handle this style of JSON without
using enums. So I need to make a bunch of enums, one for each possible tag.&lt;/p>
&lt;h3 id="the-so-many-enums-approach">The So Many Enums Approach&lt;/h3>
&lt;p>This corresponds to the
&lt;a href="https://github.com/autarch/tagged-enum-example/tree/master/with-enums">with-enums directory&lt;/a> in my
example repo.&lt;/p>
&lt;p>And here are our structs and enums:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="cp">#[derive(Debug, Deserialize)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">enum&lt;/span> &lt;span class="nc">RootWrapper&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Root&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Root&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="cp">#[derive(Debug, Deserialize)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">struct&lt;/span> &lt;span class="nc">Root&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">first&lt;/span>: &lt;span class="nc">FooWrapper&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">second&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">BarWrapper&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">actions&lt;/span>: &lt;span class="nb">Vec&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Action&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="cp">#[derive(Debug, Deserialize)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">enum&lt;/span> &lt;span class="nc">FooWrapper&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Foo&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Foo&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="cp">#[derive(Debug, Deserialize)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">struct&lt;/span> &lt;span class="nc">Foo&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">size&lt;/span>: &lt;span class="kt">i8&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">color&lt;/span>: &lt;span class="nb">String&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="cp">#[derive(Debug, Deserialize)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">enum&lt;/span> &lt;span class="nc">BarWrapper&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Bar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Bar&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="cp">#[derive(Debug, Deserialize)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">struct&lt;/span> &lt;span class="nc">Bar&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">mood&lt;/span>: &lt;span class="nb">String&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">car&lt;/span>: &lt;span class="nb">String&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="cp">#[derive(Debug, Deserialize)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">enum&lt;/span> &lt;span class="nc">Action&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Run&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">speed&lt;/span>: &lt;span class="kt">i64&lt;/span> &lt;span class="p">},&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Sleep&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">hours&lt;/span>: &lt;span class="kt">i8&lt;/span> &lt;span class="p">},&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And our &lt;code>main()&lt;/code> is:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fn&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">output&lt;/span>: &lt;span class="nc">RootWrapper&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">serde_json&lt;/span>::&lt;span class="n">from_str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="no">DOC&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="n">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;parsed&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="fm">println!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;&lt;/span>&lt;span class="si">{:#?}&lt;/span>&lt;span class="s">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">output&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Note that the type of &lt;code>output&lt;/code> is now &lt;code>RootWrapper&lt;/code> instead of &lt;code>Root&lt;/code>. This runs without an error,
giving us:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Root(
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Root {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> first: Foo(
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Foo {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> size: 42,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> color: &amp;#34;blue&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ),
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> second: Some(
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Bar(
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Bar {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> mood: &amp;#34;indigo&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> car: &amp;#34;Super&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ),
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ),
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> actions: [
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Run {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> speed: 84,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Sleep {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> hours: 8,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Yay, it works! But it has tons of pointless enums. Boo!&lt;/p>
&lt;p>The enums generally clutter up the code with a lot of destructuring. For example, if I want to get
the struct corresponding to &lt;code>$.Root.first.Foo&lt;/code>, I have to write this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RootWrapper&lt;/span>::&lt;span class="n">Root&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">root&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">output&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FooWrapper&lt;/span>::&lt;span class="n">Foo&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">foo&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">root&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">first&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>In my Pg formatting code, multiply that destructuring by a thousand.&lt;/p>
&lt;h2 id="there-must-be-some-way-out-of-here">There must be some way out of here&lt;/h2>
&lt;p>When I
&lt;a href="https://www.reddit.com/r/rust/comments/m51oet/writing_a_postgres_sql_pretty_printer_in_rust/">shared this in /r/rust&lt;/a>
last week, &lt;a href="https://www.reddit.com/user/nicoburns/">/u/nicoburns&lt;/a> had some helpful suggestions for
working around this. We
&lt;a href="https://www.reddit.com/r/rust/comments/m51oet/writing_a_postgres_sql_pretty_printer_in_rust/gr0f8wj">went back and forth a bit&lt;/a>
and I was able to get something that worked a little bit. But it only worked for simple cases. I
couldn&amp;rsquo;t get it to work for cases like &lt;code>Option&amp;lt;Bar&amp;gt;&lt;/code> or &lt;code>Vec&amp;lt;Action&amp;gt;&lt;/code>. And in the Pg parser AST, I
also end up with &lt;code>Option&amp;lt;Vec&amp;lt;Something&amp;gt;&amp;gt;&lt;/code> too, as well as cases with tuple structs like
&lt;code>Vec&amp;lt;(Foo, Bar)&amp;gt;&lt;/code> and probably some other weird things too.&lt;/p>
&lt;p>What I would love is a solution that changes the code generated by the serde macros to just &amp;ldquo;skip
over&amp;rdquo; the tag instead of creating an enum for it when the enum only has one variant.&lt;/p>
&lt;p>A solution that still requires the wrappers and even more generated code for them would be fine,
though I suspect it&amp;rsquo;d make the AST code&amp;rsquo;s slow compilation even slower.&lt;/p>
&lt;p>I started digging into serde a bit to try to understand how I might do this, but it&amp;rsquo;s pretty
complex, and I&amp;rsquo;m still pretty new to Rust.&lt;/p>
&lt;p>For now, I have enough other things to work on with this project. For example, the way I generate
formatted SQL is horrific and unscalable (lots of inline &lt;code>some_str.push_str(&amp;quot;WHERE &amp;quot;)&lt;/code> and
&lt;code>format!&lt;/code>). I&amp;rsquo;m starting on a refactor to generate some sort of intermediate representation of the
AST that I can then turn into a string.&lt;/p>
&lt;h2 id="next-up">Next up&lt;/h2>
&lt;p>Here&amp;rsquo;s a list of what I want to cover in future posts.&lt;/p>
&lt;ul>
&lt;li>Diving into the Postgres grammar to understand the AST.&lt;/li>
&lt;li>&lt;a href="https://blog.urth.org/2021/04/24/writing-a-postgres-sql-pretty-printer-in-rust-part-2/">How I&amp;rsquo;m approaching tests for this project, and how I generate test cases from the Postgres
documentation&lt;/a>.&lt;/li>
&lt;li>The benefits of Rust pattern-matching for working with ASTs.&lt;/li>
&lt;li>How terrible my initial solution to generating SQL in the pretty printer is, and how I fixed it
(once I actually fix it).&lt;/li>
&lt;li>How the proc macro in the &lt;code>bitflags_serde_int&lt;/code> crate works&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>.&lt;/li>
&lt;li>Who knows what else?&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://blog.urth.org/index.xml">Stay tuned&lt;/a> for more posts in the future.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Ok, technically I &lt;em>could&lt;/em> do that, but that would involve parsing the JSON and rewriting it in
order to &amp;hellip; make it easier to parse?&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Edit 2021-04-24: Nope, not gonna write about this. It turns out I was reimplementing the already
existing &lt;code>#[serde(transparent)]&lt;/code> feature.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Writing a Postgres SQL Pretty Printer in Rust: Part 1</title><link>https://blog.urth.org/2021/03/14/writing-a-postgres-sql-pretty-printer-in-rust-part-1/</link><pubDate>Sun, 14 Mar 2021 12:48:04 -0500</pubDate><guid>https://blog.urth.org/2021/03/14/writing-a-postgres-sql-pretty-printer-in-rust-part-1/</guid><description>&lt;p>This is the first of a planned series of blog posts about
&lt;a href="https://github.com/houseabsolute/pg-pretty">my pg-pretty project&lt;/a>. I&amp;rsquo;ll cover some things I&amp;rsquo;ve
learned about Rust and Postgres SQL, as well as some things I still don&amp;rsquo;t know.&lt;/p>
&lt;h2 id="series-links">Series Links&lt;/h2>
&lt;ul>
&lt;li>Part 1: &lt;strong>Introduction to the project and generating Rust with Perl&lt;/strong>&lt;/li>
&lt;li>Part 1.5: &lt;a href="https://blog.urth.org/2021/03/21/writing-a-postgres-sql-pretty-printer-in-rust-part-1-5/">More about enum wrappers and Serde&amp;rsquo;s externally tagged enum representation&lt;/a>&lt;/li>
&lt;li>Part 2: &lt;a href="https://blog.urth.org/2021/04/24/writing-a-postgres-sql-pretty-printer-in-rust-part-2/">How I&amp;rsquo;m testing the pretty printer and how I generate tests from the Postgres
docs&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="why">Why?&lt;/h2>
&lt;p>I really, &lt;em>really&lt;/em>, &lt;strong>really&lt;/strong>, &lt;strong>&lt;em>really&lt;/em>&lt;/strong> cannot stand unformatted code, or a mishmash of code
styles throughout a codebase. But at the same time, rejecting PRs from other developers at $WORK
just because of code formatting is not okay. Making them manually fiddle with formatting is not a
good use of their time (or mine).&lt;/p>
&lt;p>This is why we have linters, tidiers, and &lt;a href="https://blog.urth.org/2020/05/08/comparing-code-quality-meta-tools/">meta code quality tools&lt;/a> like
&lt;a href="https://github.com/houseabsolute/precious">my precious&lt;/a>.&lt;/p>
&lt;p>Combine these with a commit hook and CI checks for code cleanliness, and I never have to reject a PR
for formatting. Instead, it gets auto-&amp;ldquo;rejected&amp;rdquo; by &lt;code>git commit&lt;/code> or CI, and I&amp;rsquo;m off the hook.&lt;/p>
&lt;p>And besides the value of not annoying me, there is also value to enforcing code formatting rules
throughout a large codebase. Consistency eliminates a potential distraction, because every
&lt;a href="https://pkg.go.dev/golang.org/x/tools/cmd/goimports">Go&lt;/a>,
&lt;a href="https://metacpan.org/release/Perl-Tidy">Perl&lt;/a>, or &lt;a href="https://github.com/psf/black">Python&lt;/a> file in
the codebase will look like every other Go, Perl, or Python file.&lt;/p>
&lt;p>SQL is code, so it sure would be nice to do the same thing there, but I can&amp;rsquo;t. There are a few SQL
pretty printing tools that I&amp;rsquo;ve found, but none of them handle Postgres-specific idioms.&lt;/p>
&lt;p>So of course I should write one!&lt;/p>
&lt;p>And I should write one in Rust! Of course?&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/p>
&lt;h2 id="where-to-start">Where to Start?&lt;/h2>
&lt;p>Writing a Postgres SQL parser from scratch would be quite painful&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>. Fortunately, a lot of the
hard lower level work has already been done.&lt;/p>
&lt;p>At the very lowest level we have &lt;a href="https://github.com/lfittl/libpg_query">libpg_query&lt;/a>, created by
&lt;a href="https://github.com/lfittl">Lukas Fittl&lt;/a>. This is a project to rip the parser out of the Postgres
source tree and turn it into a C library. It&amp;rsquo;s a shame that the Postgres source is not already
organized this way. But I imagine that the parser started off as an integral part of the Postgres
codebase, and by the time anyone thought of extracting it, it was more work than anyone wanted to
take on.&lt;/p>
&lt;p>The next step is to create a Rust wrapper around this C library. Luckily that was already done too.
I&amp;rsquo;m using &lt;a href="https://lib.rs/crates/libpg_query-sys">libpg_query-sys&lt;/a>, which is a bare bones wrapper
around the C library. It exposes the same types and functions as the C library, but in Rust.&lt;/p>
&lt;h2 id="from-c-to-rust">From C to Rust&lt;/h2>
&lt;p>These underlying tools work by parsing a string containing Postgres SQL and returning a string
containing JSON. That JSON represents the
&lt;a href="https://en.wikipedia.org/wiki/Abstract_syntax_tree">AST (Abstract Syntax Tree)&lt;/a> of the parsed SQL.&lt;/p>
&lt;p>But to actually &lt;em>do&lt;/em> anything with that AST, you want native Rust structs, not a giant JSON blob.&lt;/p>
&lt;p>And that&amp;rsquo;s where my work started.&lt;/p>
&lt;p>The libpg_query source has a handy directory containing JSON files describing various parts of the
AST. For example, the
&lt;a href="https://github.com/lfittl/libpg_query/blob/10-latest/srcdata/nodetypes.json">&lt;code>nodestypes.json&lt;/code>&lt;/a>
file defines all of the possible nodes. Many parts of the AST reference the &lt;code>Node&lt;/code> type, which is
basically &amp;ldquo;any valid bit of SQL&amp;rdquo;.&lt;/p>
&lt;p>But the most important file is
&lt;a href="https://github.com/lfittl/libpg_query/blob/10-latest/srcdata/struct_defs.json">&lt;code>struct_defs.json&lt;/code>&lt;/a>.
This file defines all the data structures we might care about, providing the name, fields, and field
types for each struct.&lt;/p>
&lt;p>Rust is a statically typed language, so we can&amp;rsquo;t just parse this stuff at runtime and generate
structs in memory. Instead, we need codegen. And since these struct definitions reference C types,
we need to translate this all into Rust!&lt;/p>
&lt;h2 id="generating-rust">Generating Rust&lt;/h2>
&lt;p>Enter my totally not-a-hacked-up-mess
&lt;a href="https://github.com/houseabsolute/pg-pretty/blob/master/tools/json-to-parser.pl">&lt;code>json-to-parser.pl&lt;/code>&lt;/a>
script.&lt;/p>
&lt;p>For each C struct that we care about we generate a corresponding Rust struct&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>. This mostly means
translating from C types to Rust types. To make things extra fun, I try to make the types more
specific wherever I can. There are a number of places where the C struct just uses &lt;code>Node*&lt;/code>, but in
reality only a limited subset of nodes are valid.&lt;/p>
&lt;p>I&amp;rsquo;ve figured this out a couple ways. Sometimes, the comment for the field (which is in the
&lt;code>struct_defs.json&lt;/code> file) actually tells me. For example, many comments include the text &amp;ldquo;list of
Value strings&amp;rdquo;, which means it&amp;rsquo;s a list of strings. For whatever reason, the Postgres C code just
uses &lt;code>List*&lt;/code> (an array of &lt;code>Node*)&lt;/code> here instead of &lt;code>String*&lt;/code>&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>As an aside, I turn all the comments in the &lt;code>struct_defs.json&lt;/code> file into Rust documentation comments
in the generated code, which has been quite helpful. This lets me read the generated AST code and
get a pretty good understanding of what each struct and field contains.&lt;/p>
&lt;p>But in Rust, we really want to know what our possible types are. That&amp;rsquo;s because I&amp;rsquo;m using Rust&amp;rsquo;s
enum-based pattern matching. The &lt;code>Node&lt;/code> enum has over 100 variants. That&amp;rsquo;s a lot of matching!&lt;/p>
&lt;p>I also need to generate enum wrappers around many structs. Any time a struct references another
struct, I need the wrapper indirection. So for example, here&amp;rsquo;s a little bit of the &lt;code>DeleteStmt&lt;/code>
struct:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#[skip_serializing_none]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="cp">#[derive(Debug, Deserialize, PartialEq)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">struct&lt;/span> &lt;span class="nc">DeleteStmt&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// relation to delete from
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">relation&lt;/span>: &lt;span class="nc">RangeVarWrapper&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// RangeVar*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// ... more fields ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The &lt;code>relation&lt;/code> field is going to contain a &lt;code>RangeVarWrapper&lt;/code>, which is a one-variant enum that looks
like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#[derive(Debug, Deserialize, PartialEq)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">enum&lt;/span> &lt;span class="nc">RangeVarWrapper&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">RangeVar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">RangeVar&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="why-the-wrapper">Why the Wrapper?&lt;/h3>
&lt;p>The wrappers are annoying, and I&amp;rsquo;d like to get rid of them, but I can&amp;rsquo;t figure out how!&lt;/p>
&lt;p>Let&amp;rsquo;s take a very simple &lt;code>DELETE&lt;/code> statement and parse it:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">DELETE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">films&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The parser gives us this (with some outer bits removed for simplicity):&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;DeleteStmt&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;relation&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;RangeVar&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;inh&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;location&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;relname&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;films&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;relpersistence&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;p&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>There&amp;rsquo;s a lot to look at there, so let&amp;rsquo;s zoom in on one part:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;DeleteStmt&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;relation&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;RangeVar&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="err">...&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>We need to deserialize this into a Rust struct. For deserialization in Rust I&amp;rsquo;m using
&lt;a href="https://serde.rs/">&lt;code>serde&lt;/code>&lt;/a>, which is a powerful Rust framework for deserialization that supports
many data formats, including JSON.&lt;/p>
&lt;p>The particular structure of the JSON above corresponds to what the serde docs call
&lt;a href="https://serde.rs/enum-representations.html">&amp;ldquo;the externally tagged enum representation&amp;rdquo;&lt;/a>. In this
format, the &amp;ldquo;tags&amp;rdquo; such as &lt;code>DeleteStmt&lt;/code> and &lt;code>RangeVar&lt;/code> are used to indicate which enum variant to
deserialize to. A variant of what? Well, that&amp;rsquo;s the problem.&lt;/p>
&lt;p>As far as I can tell, the only way to make this work is to make an enum wrapper for every single
struct which might be contained in any other struct. So for the &lt;code>RangeVar&lt;/code> struct I need this
wrapper:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#[derive(Debug, Deserialize, PartialEq)]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">enum&lt;/span> &lt;span class="nc">RangeVarWrapper&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">RangeVar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">RangeVar&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And then when I&amp;rsquo;m working with the delete statement, I need to pattern match &lt;code>RangeVar&lt;/code> struct out
of the &lt;code>DeleteStmt&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fn&lt;/span> &lt;span class="nf">format_delete_stmt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="k">mut&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">d&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="nc">DeleteStmt&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="nc">R&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RangeVarWrapper&lt;/span>::&lt;span class="n">RangeVar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">relation&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// .. do something with the RangeVar in r
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I really don&amp;rsquo;t like this pattern, but from my reading so far I haven&amp;rsquo;t seen a simple way to
eliminate it. I &lt;em>think&lt;/em> the only way to do this would be to provide custom serde deserialization
logic for every struct which contains another struct.&lt;/p>
&lt;p>This is absolutely possible, but I&amp;rsquo;ve avoided this so far in order to focus on other aspects of the
project. But I want to come back to this in the future, because these wrappers require a lot of
extra pattern matching in the formatter code.&lt;/p>
&lt;h3 id="so-">So &amp;hellip;&lt;/h3>
&lt;p>So that&amp;rsquo;s why I need a Perl script to generate Rust code, though I can think of at least a couple
other approaches.&lt;/p>
&lt;p>One would be to rewrite the Perl in Rust. That would work, but the Perl script is already fast. The
naive Rust approach would probably be slower, since I would have to re-compile the Rust generator
code every time I changed it, though I could ameliorate that by moving some data to config files.
But Perl is a great language for reading JSON and generating code.&lt;/p>
&lt;p>Another, almost certainly terrible option, would be to write one or more macros that could read the
JSON source data and generate the Rust code directly. I&amp;rsquo;m fairly sure this is possible with
&lt;a href="https://doc.rust-lang.org/reference/procedural-macros.html">procedural macros&lt;/a>. A procedural macro
looks like a function call or an attribute when you use it. The implementation is just regular Rust
code that takes either its &amp;ldquo;function&amp;rdquo; arguments as input, or the thing that they are an attribute of
(a type, struct field, etc.). Either way, the macro implementation returns a new AST of Rust code
that is effectively inlined in place of the macro.&lt;/p>
&lt;p>Procedural macros are incredibly powerful, and I
&lt;del>&lt;a href="https://github.com/houseabsolute/pg-pretty/tree/master/bitflags_serde_int">wrote one to change how bitflags are serialized&lt;/a>
so that serde expects these flags to be integers during deserialization, rather than expecting a
JSON object like &lt;code>{ &amp;quot;bits&amp;quot;: 42 }&lt;/code>&lt;/del> later realized that &lt;code>serde&lt;/code> already did what I needed, so I
didn&amp;rsquo;t need to write that wrapper. The &lt;a href="https://lib.rs/crates/bitflags">bitflags crate&lt;/a> itself is a
proc macro, &lt;del>so it&amp;rsquo;s macros all the way down&lt;/del>.&lt;/p>
&lt;p>But a procedural macro that parses arbitrary JSON files to generate Rust code seems a bit gross&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup>.
And right now I find myself constantly referring to the generated SQL AST structs. Having those
available as regular Rust code that I can examine in my editor is very helpful.&lt;/p>
&lt;h2 id="can-you-try-it-out">Can You Try it Out?&lt;/h2>
&lt;p>Err, sort of. If you want to give it a whirl you can
&lt;a href="https://github.com/houseabsolute/pg-pretty">clone the repo&lt;/a>, then edit the contents of
&lt;a href="https://github.com/houseabsolute/pg-pretty/blob/master/cli/src/main.rs">&lt;code>cli/src/main.rs&lt;/code>&lt;/a>, which
has some SQL to be formatted in it. But I haven&amp;rsquo;t actually built a proper CLI for it yet. I&amp;rsquo;ve just
been focused on the core formatting implementation, which I exercise through its test suite.&lt;/p>
&lt;h2 id="coming-soon">Coming Soon&lt;/h2>
&lt;p>This post is already quite long, but there are many other things I&amp;rsquo;ve learned while working on this
project that I plan to write about, including:&lt;/p>
&lt;ul>
&lt;li>Diving into the Postgres grammar to understand the AST.&lt;/li>
&lt;li>&lt;a href="https://blog.urth.org/2021/04/24/writing-a-postgres-sql-pretty-printer-in-rust-part-2/">How I&amp;rsquo;m approaching tests for this project, and how I generate test cases from the Postgres
documentation&lt;/a>.&lt;/li>
&lt;li>The benefits of Rust pattern-matching for working with ASTs.&lt;/li>
&lt;li>How terrible my solution to generating SQL in the pretty printer is, and how I wonder if there&amp;rsquo;s a
better way to do this.&lt;/li>
&lt;li>How the proc macro in the &lt;code>bitflags_serde_int&lt;/code> crate works&lt;sup id="fnref:6">&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref">6&lt;/a>&lt;/sup>.&lt;/li>
&lt;li>Who knows what else?&lt;/li>
&lt;/ul>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Rust turned out to be a great fit for this project. More on that in a future post.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>That&amp;rsquo;s an understatement. It would be a mammoth project of its own.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>I figured out what to care about by a combination of trial and error and experimentation. The
&lt;code>struct_defs.json&lt;/code> file organizes structs based on what files they&amp;rsquo;re defined in. I was able to
determine that (so far) I only care about types from a small subset of these files.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>Probably because if you&amp;rsquo;re writing the parser and the thing that consumes it at the same time,
you can write code that knows that it&amp;rsquo;s only a String. Also, C doesn&amp;rsquo;t have Rust&amp;rsquo;s exhaustive
pattern matching, so you&amp;rsquo;re not forced to deal with all possible Node types.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>More than a bit. Really, really gross.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:6">
&lt;p>Edit 2021-04-24: Nope, not gonna write about this. It turns out I was reimplementing the already
existing &lt;code>#[serde(transparent)]&lt;/code> feature.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>2020 Predictions Reviewed</title><link>https://blog.urth.org/2021/01/29/2020-predictions-reviewed/</link><pubDate>Fri, 29 Jan 2021 16:48:56 -0600</pubDate><guid>https://blog.urth.org/2021/01/29/2020-predictions-reviewed/</guid><description>&lt;p>&lt;a href="https://blog.urth.org/2020/05/02/predictions-for-2020/">Last year in May I made some predictions&lt;/a>. Now it&amp;rsquo;s time to find out how I did!&lt;/p>
&lt;h2 id="summary">Summary&lt;/h2>
&lt;p>The summary is I was wrong. A lot. This should be no surprise.&lt;/p>
&lt;p>First, let&amp;rsquo;s take a look at my overall accuracy:&lt;/p>
&lt;iframe
title="Accuracy Chart"
src="https://docs.google.com/spreadsheets/d/e/2PACX-1vSAvBXVxXuWDcGKwlWrX2bi0tgYTz4vMqdG-KbNCJDJgTQeXS8HoLVsUaHG_zdSUKMPE-q6Z_bB8p7Y/pubchart?oid=77640182&amp;format=interactive"
height="371"
width="600"
>&lt;/iframe>
&lt;p>The source data for this chart is a
&lt;a href="https://docs.google.com/spreadsheets/d/1Z7HsnEYPT6p-CPm-LnnIYvUkyS_zuEC_URN3rmHTT8o/edit?usp=sharing">spreadsheet I made for my 2020 predictions&lt;/a>.
Overall, this should be fairly understandable but there&amp;rsquo;s one nuance that needs some explaining. In
order to make the chart simpler, I converted any prediction for less than 50% to its inverse and
graphed that.&lt;/p>
&lt;p>Here&amp;rsquo;s an example. I estimated a 10% likelihood that &amp;ldquo;a vaccine is generally available by end of
2020&amp;rdquo;. But another way to think of it is that I estimated a 90% likelihood that a vaccine &lt;strong>is not&lt;/strong>
generally available by the end of 2020.&lt;/p>
&lt;p>If my predictions were perfect, then 60% of my predictions of 60% likelihood would have happened,
70% of 70%, and so on. But I wasn&amp;rsquo;t even close! Only 33% of my 60% predictions happened. I did even
worse at 70% (33% happened), improved a little at 80% (50%), and redeemed myself a little at 90%
(80%) and 95% (100%).&lt;/p>
&lt;p>The accuracy trendline goes in the right direction, but it&amp;rsquo;s way too steep. I clearly have work to
do on becoming an expert prognosticator.&lt;/p>
&lt;h2 id="deep-dive">Deep Dive&lt;/h2>
&lt;p>Let&amp;rsquo;s look at each of my predictions in detail. Only the text in bold is new. The rest is copied
from my original predictions post.&lt;/p>
&lt;h3 id="politics-and-economy">Politics and Economy&lt;/h3>
&lt;ol>
&lt;li>Joe Biden is the Democratic nominee come November voting: 95%
&lt;ul>
&lt;li>&lt;strong>Yup.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Trump is the Republican nominee: 95%
&lt;ul>
&lt;li>&lt;strong>Yup.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>November election proceeds normally: 95%
&lt;ul>
&lt;li>By &amp;ldquo;normally&amp;rdquo; I simply mean that the election occurs on the scheduled day in all 50 states. I
think states may also expand voting by mail, but no state will cancel or postpone the election.
I do expect some states to use the pandemic as an excuse for &lt;em>even more&lt;/em> voter suppression, but
sadly that fits the definition of &amp;ldquo;normal&amp;rdquo; in the US.&lt;/li>
&lt;li>&lt;strong>Yup. While shit went crazy &lt;em>after&lt;/em> the election, the election itself was close enough to
normal that I will count myself correct on this one.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Trump wins the November election: 60%
&lt;ul>
&lt;li>&lt;strong>Nope. I should have counted on Trump&amp;rsquo;s incredible incompetence at managing both coronavirus
and the economic downturn that it caused. I think this is what cost him the election.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Democrats maintain control of the House: 80%
&lt;ul>
&lt;li>&lt;strong>Yup.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Republicans maintain control of the Senate: 70%
&lt;ul>
&lt;li>This is mostly based on which states are having senatorial elections this year. Nearly all of
them are Republican strongholds.&lt;/li>
&lt;li>&lt;strong>Nope. But I stand by my 70%. This was incredibly close. I &lt;em>didn&amp;rsquo;t&lt;/em> predict that Trump would
completely lose his mind and engage in a series of escalatingly insane attempts to overturn the
election, which I think ended up tipping the Georgia Senate votes to the Democrats.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>US GDP for 2020 is down by at least 30% year over year compared to 2019: 80%
&lt;ul>
&lt;li>&lt;strong>Nope. Looking at &lt;a href="https://countryeconomy.com/gdp/usa">one source&lt;/a>, it shows the US economy as
down 3.5% for the year. Thinking back to this, I didn&amp;rsquo;t actually do any research on past annual
GDP changes. If I had, I would have realized that 30% in any direction is utterly ridiculous.
On the site I linked before, the biggest change was 18.9% in 1942, in the middle of WW2. Was
coronavirus a bigger event than WW2? No, it definitely was not.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h3 id="coronavirus">Coronavirus&lt;/h3>
&lt;ol start="8">
&lt;li>Minnesota lifts stay at home order but then reinstates it at least once: 90%
&lt;ul>
&lt;li>&lt;strong>Nope. I was tempted to give myself a &amp;ldquo;Yup&amp;rdquo; here, but we didn&amp;rsquo;t get a second stay-at-home
order in December. We got something that was fairly similar, but not quite the same.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Three months or more of cumulative stay at home in Minnesota: 80%
&lt;ul>
&lt;li>&lt;strong>Nope. I didn&amp;rsquo;t realize the level of insanity that would arise around restrictions, which made
it politically impossible for any governor to be this strict, regardless of whether the
circumstances warranted it. But if I thought about this a bit more deeply I think I would&amp;rsquo;ve
realized how unlikely this was.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Five months or more of cumulative stay at home in Minnesota: 20%
&lt;ul>
&lt;li>&lt;strong>Yup.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Over 100,000 in the US dead from coronavirus: 90%
&lt;ul>
&lt;li>&lt;strong>Yup.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Over 250,000 in the US dead from coronavirus: 40%
&lt;ul>
&lt;li>&lt;strong>Nope, &lt;em>more&lt;/em> than 250,000 died. Sigh.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>A vaccine is generally available by end of 2020: 10%
&lt;ul>
&lt;li>By &amp;ldquo;generally available&amp;rdquo; I mean that it&amp;rsquo;s not in clinical trials, that it&amp;rsquo;s available in
sufficient quantities for use as needed, and that it is safe to be given to at least 90% of the
population.&lt;/li>
&lt;li>&lt;strong>Yup, no vaccine was generally available by the end of 2020. But good job vaccine makers for
nearly making me wrong here!&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>A generally available therapy exists that reduces mortality by 30% or greater: 60%
&lt;ul>
&lt;li>This can be either be preventative or something that reduces severity of the symptoms.&lt;/li>
&lt;li>&lt;strong>WTF? How did I think I could possibly figure this out? Well, I just tried and I can&amp;rsquo;t.
Looking at &lt;a href="https://ourworldindata.org/mortality-risk-covid?country=~USA">Our World in Data&lt;/a>,
it seems like the case fatality rate went from a peak of 6.2% to 1.7%, but I have no idea why.
I didn&amp;rsquo;t factor this prediction into my accuracy chart because it&amp;rsquo;s not really assessable for
accuracy.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>I have had coronavirus: 40%
&lt;ul>
&lt;li>This is based on either a test or my best guess based on symptoms and contacts.&lt;/li>
&lt;li>&lt;strong>Nope, I &lt;em>haven&amp;rsquo;t&lt;/em> had it? I have had both an antibody test (November 18, 2020) and a test for
the virus (in mid-January). Both were negative. However, I, my wife, and two close friends all
got sick with what felt like very bad cold symptoms at the end of February, 2020. Maybe we had
COVID, maybe we didn&amp;rsquo;t. We&amp;rsquo;ll never know. But I counted this as a miss in my accuracy chart.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>I am hospitalized for coronavirus: 5%
&lt;ul>
&lt;li>&lt;strong>Yup, I was &lt;em>not&lt;/em> hospitalized.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>At least one person I know dies of coronavirus: 60%
&lt;ul>
&lt;li>This hinges a lot on the definition of &amp;ldquo;know&amp;rdquo;. Let&amp;rsquo;s say it&amp;rsquo;s people I&amp;rsquo;ve met in person more
than once and I remembered how I know them when I learned about their death. Public figures and
celebrities don&amp;rsquo;t count (not that I&amp;rsquo;ve met many).&lt;/li>
&lt;li>&lt;strong>Nope. I know more than a few people who&amp;rsquo;ve had it, but no one I know personally has died.
Glad to be wrong.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Fatality rate in the US is estimated at less than 1% in retrospect, excluding any newly developed
treatments from #13: 70%
&lt;ul>
&lt;li>Note that even a &amp;ldquo;low&amp;rdquo; fatality rate like this is still very dangerous when combined with rapid
spread and no vaccine/immunity.&lt;/li>
&lt;li>&lt;strong>Nope with a side of WTF. This is phrased so unclearly that I&amp;rsquo;m not sure how I would&amp;rsquo;ve
handled the whole &amp;ldquo;excluding any newly developed treatments&amp;rdquo; thing. But that&amp;rsquo;s moot since the
fatality rate is &lt;em>clearly&lt;/em> above 1%.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h3 id="personal">Personal&lt;/h3>
&lt;ol start="18">
&lt;li>I am still working for ActiveState: 95%
&lt;ul>
&lt;li>&lt;strong>Yup. And I still like working there.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>I have attended at least one non-Perl conference (in person or online): 60% _ What &amp;ldquo;attending&amp;rdquo;
means for an online conference will be on the honor system. _ &lt;strong>Yup. &lt;a href="https://blog.urth.org/2020/08/27/i-attended-rustconf-2020/">I attended RustConf
2020&lt;/a> back in August of 2020. It was great.&lt;/strong>&lt;/li>
&lt;li>I have released at least
&lt;a href="http://neilb.org/cpan-regulars/">one CPAN module every month for 20 years&lt;/a>: 80%
&lt;ul>
&lt;li>&lt;strong>Yup. &lt;a href="https://blog.urth.org/2020/12/05/twenty-years-of-cpan-releases/">I did this one&lt;/a>.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>My weight is 210 pounds or below and has been since November 1: 70%
&lt;ul>
&lt;li>See &lt;a href="https://blog.urth.org/2020/02/11/i-weigh-way-less/">I Weigh Way Less&lt;/a> for more than you ever
wanted to know about this topic. For reference I was at 216 last time I weighed myself.&lt;/li>
&lt;li>&lt;strong>Yup. My weight has been around 201-204 most times that I&amp;rsquo;ve checked over the past few months.
There were a couple of days where it dipped below 200, but that was very temporary. I&amp;rsquo;m pretty
happy with where I&amp;rsquo;ve gotten to. I don&amp;rsquo;t know how I&amp;rsquo;d lose significantly more weight at this
point without adopting a much more drastic diet change than I&amp;rsquo;m up for.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>My weight is 200 pounds or below and has been since November 1: 10%
&lt;ul>
&lt;li>&lt;strong>Yup. My weight &lt;em>was not&lt;/em> below 200 consistently.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>I have climbed at least one climbing route (top rope or lead) rated 5.11(-/a) or higher: 60%
&lt;ul>
&lt;li>This mostly reflects my prediction about access to climbing gyms and outdoor climbing this
year. Absent coronavirus I&amp;rsquo;d have put this at 95%. Note I consider this to have happened even
if I fall or rest during the climb and then continue. This is just about whether I can top out
at all.&lt;/li>
&lt;li>&lt;strong>Nope. I haven&amp;rsquo;t been back to the climbing gym since February of 2020. I did take a top rope
anchors class, and I did some outdoor top rope and lead climbing, but not nearly enough to
improve, and I haven&amp;rsquo;t been climbing in several months now. I will not be surprised if I&amp;rsquo;m not
even able to do 5.10+ next time I actually go to the gym.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>I have a bouldering wall in my garage: 80%
&lt;ul>
&lt;li>&lt;strong>Nope. My mother threw a monkey wrench into this one by dying last September. That set in
motion a plan where my father moved here and we purchased a new duplex with him. My father is
currently living in his part of it that is his, and we&amp;rsquo;re having major renovations done on our
part with a plan to move in April or May. Once that started it was obvious there was no point
in having a bouldering wall built in our current garage.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>I maintain my three hours (ish) per week weight training schedule (modulo illness or injury that
prevents me from doing so): 95%
&lt;ul>
&lt;li>There&amp;rsquo;s a little leeway here. For example, yesterday I did 45 minutes instead of 60 because of
some neck &amp;amp; shoulder issues, but I&amp;rsquo;d still count this week as a success.&lt;/li>
&lt;li>&lt;strong>Yup. I&amp;rsquo;ve had some days/weeks off because of illness and injury, but overall I&amp;rsquo;ve stuck with
my exercise plan. I&amp;rsquo;m probably doing about 150 to 165 minutes per week, which I will count as
&amp;ldquo;three hours (ish)&amp;rdquo;.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>I&amp;rsquo;m still vegan: 95%
&lt;ul>
&lt;li>If I let myself score 100% this would definitely be 100%.&lt;/li>
&lt;li>&lt;strong>Yup. In retrospect I&amp;rsquo;m not sure if this sort of prediction is worth putting on the list. I
could also put things like &amp;ldquo;I won&amp;rsquo;t be hit by a meteor&amp;rdquo; or &amp;ldquo;I will go shopping for food&amp;rdquo;. While
these are likely to be accurate predictions, they don&amp;rsquo;t really do anything to evaluate my
prognositication skills. I think I just juked the stats with this prediction.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>I spend time in Taiwan this year (whether on vacation or as a temporary move): 10%
&lt;ul>
&lt;li>&lt;strong>Yup. I &lt;em>did not&lt;/em> go to Taiwan in 2020. We&amp;rsquo;re hoping to go later this year, though.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h2 id="parting-thoughts">Parting Thoughts&lt;/h2>
&lt;p>I looked at the stats for my three categories and I didn&amp;rsquo;t see any huge difference in accuracy
between them. The only obvious highlight is that things I predicted at 95% were the most accurate.
But those were basically all sure things.&lt;/p>
&lt;p>My main takeaways are as follows:&lt;/p>
&lt;ul>
&lt;li>If my initial certainty on a prediction is lower than 90%, I probably need to do some research or
deeper thought to improve my accuracy.&lt;/li>
&lt;li>I should be careful to consider how I will actually figure out whether the predicted thing
happened. There were a couple cases where I couldn&amp;rsquo;t really figure out how to do that this year.&lt;/li>
&lt;li>I&amp;rsquo;m not &lt;a href="https://en.wikipedia.org/wiki/Superforecaster">a superforecoaster&lt;/a>. But I would&amp;rsquo;ve
predicted that to be the case, so maybe I am, at least when it comes to predicting my forecasting
abilities.&lt;/li>
&lt;/ul>
&lt;p>Stay tuned for 2021 predictions. Maybe. Right now I don&amp;rsquo;t have as many ideas for things I might
predict this year. Politics and coronavirus are (hopefully) going to be less interesting this year,
and I&amp;rsquo;m not sure there are other really interesting fields I can even attempt to make predictions
in.&lt;/p></description></item><item><title>What I did on my winter vacation</title><link>https://blog.urth.org/2021/01/09/what-i-did-on-my-winter-vacation/</link><pubDate>Sat, 09 Jan 2021 11:12:57 -0600</pubDate><guid>https://blog.urth.org/2021/01/09/what-i-did-on-my-winter-vacation/</guid><description>&lt;p>TLDR: Helped my father move. Then I shaved all the yaks. Fur everywhere. Very messy.&lt;/p>
&lt;p>Because of the way holidays at ActiveState work, it&amp;rsquo;s very economical in terms of vacation days to
take the last two weeks of the year off (Christmas and New year&amp;rsquo;s weeks). I had a fair bit of
vacation left, so I decided to take the first week of the new year off as well, for a total of three
weeks of vacation.&lt;/p>
&lt;p>So what did I do with all that time?&lt;/p>
&lt;p>Answer: Helped me father move, then wrote a lot of Rust and some Perl.&lt;/p>
&lt;h2 id="helping-my-father-move">Helping My Father Move&lt;/h2>
&lt;p>My mother died at the beginning of September. She and my father had been living in Florida, but it
didn&amp;rsquo;t make sense for my father to continue living there alone. My wife and I had been discussing
moving to a new house in Minneapolis for a while, so my father suggested we find a place where we
could all live. We were looking for either a duplex or a property with an ADU&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>. We found a
perfect odd duplex&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> very quickly. The sale closed on December 16. My father&amp;rsquo;s stuff from Florida
arrived a few days later, and he moved in to his part of the duplex.&lt;/p>
&lt;p>Unfortunately, because of COVID, we didn&amp;rsquo;t go down to Florida to help him prepare for the move.
While he did sort through some of his stuff, his sorting could&amp;rsquo;ve been more aggressive, as he was
moving from a large house with two people to a much smaller space with one person. The upshot is
that he moved a ridiculous amount of stuff here, far more than could ever fit in his space. So we
spent many hours going through it.&lt;/p>
&lt;p>We still have a ridiculous number of boxes of stuff to give away in the main part of the house,
along with an endless pile of cardboard and packing paper, but his unit in the duplex is looking
great.&lt;/p>
&lt;p>This made me even more enthusiastic about getting rid of tons of stuff before my wife and I move,
which will happen later this year, after some renovations to our part of the duplex.&lt;/p>
&lt;h2 id="my-local-covid-tracker">My Local COVID Tracker&lt;/h2>
&lt;p>I&amp;rsquo;ve &lt;a href="https://blog.urth.org/2020/12/12/my-local-covid-stats-tracker/">posted about this&lt;/a> a &lt;a href="https://blog.urth.org/2020/12/19/the-covid-urth-org-rube-goldberg-machine/">couple times&lt;/a> already. I made an update to track more counties,
as well as to add a graph
&lt;a href="https://covid.urth.org">showing the seven-day new case average per 10,000 people&lt;/a>.&lt;/p>
&lt;p>The new graph by population made it clear that the counties near me were doing &lt;em>worse&lt;/em> than the
state as a whole. The original graph, showing just the raw count of new cases, made it look like the
state overall was worse than the nearby counties, when in fact I think the opposite is true.&lt;/p>
&lt;h2 id="precious-and-my-yak-shaving-expedition">Precious and My Yak Shaving Expedition&lt;/h2>
&lt;p>&lt;a href="https://github.com/houseabsolute/precious">Precious is a project&lt;/a> is a project I started to create
a meta-linter/tidier in Rust. The goal is to replace
&lt;a href="https://metacpan.org/release/Code-TidyAll">TidyAll&lt;/a>. I&amp;rsquo;ve written about &lt;a href="https://blog.urth.org/2020/04/25/the-real-dirt-on-tidyall/">TidyAll&amp;rsquo;s
issues&lt;/a> in the past.&lt;/p>
&lt;h3 id="switching-datetime-to-use-precious">Switching DateTime to use Precious&lt;/h3>
&lt;p>I&amp;rsquo;ve &lt;a href="https://github.com/houseabsolute/precious/blob/master/precious.toml">used &lt;code>precious&lt;/code>&lt;/a> for a
few projects, including itself, but I wanted to try moving a Perl project to it. I picked
&lt;a href="https://github.com/houseabsolute/DateTime.pm">DateTime&lt;/a> for no particular reason, other than that
it&amp;rsquo;s something I&amp;rsquo;ve worked on for many years.&lt;/p>
&lt;p>This is what led me down the yak shaving rabbit hole, to mix some metaphors.&lt;/p>
&lt;p>First, I found some bugs with &lt;code>precious&lt;/code> and made
&lt;a href="https://github.com/houseabsolute/precious/releases/tag/v0.0.7">a v0.0.7 release&lt;/a> of it.&lt;/p>
&lt;p>Then when I started working on converting DateTime from TidyAll to &lt;code>precious&lt;/code>, I found several bugs
in &lt;a href="https://github.com/houseabsolute/omegasort">&lt;code>omegasort&lt;/code>&lt;/a>&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>, leading to a
&lt;a href="https://github.com/houseabsolute/omegasort/releases/tag/v0.0.4">new release of that&lt;/a> as well.&lt;/p>
&lt;p>As I worked on switching DateTime to use &lt;code>precious&lt;/code>, I realized there was a bootstrapping problem.
With TidyAll, I can just specify TidyAll and any needed plugins as develop phase prereqs for the
distribution, making it easy for others to install all the needed tools, like
&lt;a href="https://metacpan.org/release/Perl-Tidy">perltidy&lt;/a>,
&lt;a href="https://metacpan.org/release/Perl-Critic">perlcritic&lt;/a>, and TidyAll itself.&lt;/p>
&lt;p>But &lt;code>precious&lt;/code> isn&amp;rsquo;t on CPAN, nor is &lt;code>omegasort&lt;/code>. I did briefly consider going down the whole
&lt;a href="https://metacpan.org/pod/Alien">Alien&lt;/a> route, but quickly discarded that idea. While depending on a
notional &lt;code>Alien::precious&lt;/code> would work fine for my Perl projects, it wouldn&amp;rsquo;t help for anything
that&amp;rsquo;s not Perl.&lt;/p>
&lt;p>&lt;em>Note that &lt;code>precious&lt;/code>, being a Rust project, produces a single statically linked&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup> binary. Go
programs like &lt;code>omegasort&lt;/code> are totally static, not even linking libc. This will become more important
later in my yak shaving journey.&lt;/em>&lt;/p>
&lt;h3 id="just-one-little-installer">Just One Little Installer&lt;/h3>
&lt;p>So my next thought was to build a simple installer for &lt;code>precious&lt;/code> that could be run using the very
safe &amp;ldquo;pipe &lt;code>curl&lt;/code> output into an interpreter&amp;rdquo; strategy. I quickly wrote an installer in Perl that
would live in the &lt;code>precious&lt;/code> repo. I was using
&lt;a href="https://metacpan.org/release/App-FatPacker">fatpack&lt;/a>, which is a great tool for turning Perl
programs into single-file executables, as long as all of its dependencies are pure Perl.&lt;/p>
&lt;p>But then I started thinking about &lt;code>omegasort&lt;/code>. Did I want to write a nearly identical program to
live in the &lt;code>omegasort&lt;/code> repo? And then do it again for my next Rust or Go project? Plus there are
lots of other useful tools that fall in this &amp;ldquo;released as a single binary on GitHub&amp;rdquo; bucket, like
&lt;a href="https://github.com/BurntSushi/ripgrep">ripgrep&lt;/a>.&lt;/p>
&lt;h3 id="just-_one_-installer">Just &lt;em>One&lt;/em>(!) Installer&lt;/h3>
&lt;p>Then I had an idea. What if I wrote &lt;em>one&lt;/em> installer for all these things? So I made a little Perl
distribution called &lt;code>App-ugri&lt;/code>, where &amp;ldquo;ugri&amp;rdquo; stood for Universal GitHub Release Installer. I
actually finished that before I realized the critical flaw in my plan. Even though I could fatpack
&lt;code>ugri&lt;/code> into a single file so you could pipe &lt;code>curl&lt;/code> into &lt;code>perl&lt;/code>, what about Windows?&lt;/p>
&lt;p>Then I remembered why I was creating this installer in the first place. It&amp;rsquo;s because of languages
like Rust and Go that produce a single statically linked executable. The solution was pretty
obvious. Write this installer in Rust.&lt;/p>
&lt;h3 id="ubi">ubi&lt;/h3>
&lt;p>&amp;ldquo;UBI&amp;rdquo; stands for Universal Binary Installer. I liked the pun here more than the original &amp;ldquo;ugri&amp;rdquo;
name. Of course, by &amp;ldquo;universal binary installer&amp;rdquo; I mean it just installs single-file executables
from GitHub project releases, so it&amp;rsquo;s not very universal (yet?).&lt;/p>
&lt;p>Since I&amp;rsquo;d already written this once in Perl, writing a Rust version was mostly pretty easy. The only
wrinkle was that I had to learn a little bit about async programming in Rust, because the GitHub API
client I was using, &lt;a href="https://github.com/XAMPPRocky/octocrab">&lt;code>octocrab&lt;/code>&lt;/a>, is async. This was
something I&amp;rsquo;d been wanting to learn about anyway, so I welcomed the challenge.&lt;/p>
&lt;p>There is a usable 0.0.2 release on the
&lt;a href="https://github.com/houseabsolute/ubi/releases">GitHub project&amp;rsquo;s releases page&lt;/a>.&lt;/p>
&lt;p>Of course, ubi has a bootstrapping problem. What do you install a universal binary installer with?
You &lt;code>curl&lt;/code> a script into &lt;code>sh&lt;/code>, of course&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup>!&lt;/p>
&lt;p>That looks like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$&amp;gt; curl --silent --location \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> https://raw.githubusercontent.com/houseabsolute/ubi/master/bootstrap/bootstrap-ubi.sh |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>That should work on Linux and macOS. But I would love
&lt;a href="https://github.com/houseabsolute/ubi/issues/1">some help with creating the equivalent PowerShell script for Windows&lt;/a>.
I also need to improve the release tooling to provide binaries for more systems.&lt;/p>
&lt;h4 id="installing-precious-and-omegasort">Installing &lt;code>precious&lt;/code> and &lt;code>omegasort&lt;/code>&lt;/h4>
&lt;p>Once you have &lt;code>ubi&lt;/code> installed, installing these tools is trivial:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$&amp;gt; ubi --project houseabsolute/precious --in ~/bin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$&amp;gt; ubi --project houseabsolute/omegasort --in ~/bin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># and for good measure
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$&amp;gt; ubi --project BurntSushi/ripgrep --exe rg --in ~/bin
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="but-wait-theres-more-yak">But wait, there&amp;rsquo;s more yak!&lt;/h3>
&lt;p>Along the way, I also ended up working on
&lt;a href="https://metacpan.org/release/Dist-Zilla-PluginBundle-DROLSKY">my &lt;code>dzil&lt;/code> bundle&lt;/a> to to make
switching to &lt;code>precious&lt;/code> easier. So now my bundle will:&lt;/p>
&lt;ul>
&lt;li>Generate a &lt;code>precious.toml&lt;/code> config file for any project which doesn&amp;rsquo;t yet have one, as long as
there&amp;rsquo;s no &lt;code>tidyall.ini&lt;/code> file either.&lt;/li>
&lt;li>Generate an extended test that runs &lt;code>precious lint --all&lt;/code> and makes sure there are no files that
fail the linting checks.&lt;/li>
&lt;li>Generate a simple &lt;code>dev-bin/install-xt-tool.sh&lt;/code> script that installs &lt;code>ubi&lt;/code>, then uses that to
install &lt;code>precious&lt;/code> and &lt;code>omegasort&lt;/code>.&lt;/li>
&lt;li>Generate a git hook script that uses &lt;code>precious&lt;/code>, along with a &lt;code>git/setup.pl&lt;/code> script to install
that hook for the given repo.&lt;/li>
&lt;li>Update the &lt;code>dist.ini&lt;/code> to always include an &lt;code>authordep&lt;/code> on the version of the bundle that is being
run.&lt;/li>
&lt;/ul>
&lt;p>And since I was messing with all this, I added
&lt;a href="https://metacpan.org/release/Pod-Checker/">&lt;code>podchecker&lt;/code>&lt;/a> and
&lt;a href="https://metacpan.org/release/Pod-Tidy">&lt;code>podtidy&lt;/code>&lt;/a> to my standard &lt;code>precious&lt;/code> config for Perl
projects.&lt;/p>
&lt;p>That last bullet point, about updating the &lt;code>dist.ini&lt;/code> file, came out of some issues I found in
trying to get precious tests passing in CI using my
&lt;a href="https://github.com/houseabsolute/ci-perl-helpers">&lt;code>ci-perl-helpers&lt;/code>&lt;/a> tooling for Azure Pipelines.
I&amp;rsquo;ve &lt;a href="https://blog.urth.org/2019/11/18/my-new-ci-helpers-for-perl/">written about those&lt;/a> in the past as well. I ended up
&lt;a href="https://github.com/houseabsolute/ci-perl-helpers/releases">making some improvements to the helpers&lt;/a>,
so now they&amp;rsquo;ll automatically run the &lt;code>dev-bin/install-xt-tools.sh&lt;/code> script before running extended
tests.&lt;/p>
&lt;p>And when they&amp;rsquo;re building a tarball for any Perl distro where the name starts with &amp;ldquo;Dist-Zilla&amp;rdquo;,
they make sure to include the git checkout&amp;rsquo;s &lt;code>lib&lt;/code> directory in &lt;code>@INC&lt;/code> when running &lt;code>dzil build&lt;/code>. I
assume that if you&amp;rsquo;re testing a &lt;code>dzil&lt;/code> plugin or plugin bundle in CI, then you want to use said
plugin or bundle when generating the tarball for said plugin or bundle&lt;sup id="fnref:6">&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref">6&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>The results of all of this can be seen in
&lt;a href="https://github.com/houseabsolute/DateTime.pm/pull/117">my PR to switch DateTime to &lt;code>precious&lt;/code>&lt;/a>.&lt;/p>
&lt;h3 id="other-random-bits">Other Random Bits&lt;/h3>
&lt;ul>
&lt;li>I tweaked &lt;a href="https://github.com/unicode-org/cldr/pull/885">a PR for the CLDR project&lt;/a> (Common Locale
Data Repository) to fix a typo in one language&amp;rsquo;s datetime info.&lt;/li>
&lt;li>I submitted &lt;a href="https://github.com/XAMPPRocky/octocrab/pull/63">a PR to octocrab&lt;/a> to update its
dependencies so &lt;em>I&lt;/em> could update the same deps in &lt;code>ubi&lt;/code>.&lt;/li>
&lt;li>I made a new release of &lt;a href="https://metacpan.org/release/DateTime-Locale">DateTime-Locale&lt;/a> to add
some more documentation.&lt;/li>
&lt;li>I almost started rewriting omegasort in Rust (just because I like it better than Go), but I
managed to restrain myself. I might get back to this at some point, but I&amp;rsquo;m glad I worked on these
other projects for now.&lt;/li>
&lt;li>I wrote this blog post.&lt;/li>
&lt;/ul>
&lt;h2 id="putting-the-yaks-to-bed">Putting the Yaks to Bed&lt;/h2>
&lt;p>I wrapped this all up yesterday, more or less. I return to work on Monday, so my timing was pretty
good.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Accessory Dwelling Unit - think of an apartment built over a garage.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>It&amp;rsquo;s not a normal duplex. Instead of a house split into two pieces, it&amp;rsquo;s an older three story
house, built in 1915, with a newer, much smaller two story &amp;ldquo;apartment&amp;rdquo; attached to the back.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>Because TidyAll is in Perl, sorting plugins for it are just simple Perl classes. But &lt;code>precious&lt;/code>
only invokes other executables, so I realized I needed a sorting tool soon after starting on
&lt;code>precious&lt;/code>. I wanted to write the sorting tool in Rust but at the time I started, Rust had no
support for Unicode collation, so I wrote it in Go instead.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>Except for libc.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>But it can install itself if you already have it installed.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:6">
&lt;p>Does that sentence make any sense? I&amp;rsquo;ve lost track. There&amp;rsquo;s too much yak fur in here!&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>My New Rube Goldberg Machine</title><link>https://blog.urth.org/2020/12/19/the-covid-urth-org-rube-goldberg-machine/</link><pubDate>Sat, 19 Dec 2020 22:30:10 -0600</pubDate><guid>https://blog.urth.org/2020/12/19/the-covid-urth-org-rube-goldberg-machine/</guid><description>&lt;p>My last post was about my &lt;a href="https://blog.urth.org/2020/12/12/my-local-covid-stats-tracker/">local COVID tracker tool&lt;/a>. While it worked well, I found having to re-run the
&lt;code>report.pl&lt;/code> script every time I wanted an update annoying. Plus, I wanted to share this on Facebook,
but I have non-technical friends who would not be able to run it for themselves.&lt;/p>
&lt;p>So I decided to put up a hosted version, but I challenged myself. I wanted it to run entirely on
someone else&amp;rsquo;s machines. And I didn&amp;rsquo;t want to pay for it.&lt;/p>
&lt;p>So how to do it?&lt;/p>
&lt;p>Well, the hosting is simple. I&amp;rsquo;ve been using &lt;a href="https://render.com/">Render&lt;/a> for this blog, as well as
&lt;a href="https://www.houseabsolute.com/">my professional site&lt;/a> and some other static sites&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>. And while my
COVID tracker does require updated data to stay relevant, the data is just a simple JSON file and
the chart is generated entirely in the browser.&lt;/p>
&lt;p>So the trick was to make the data file available in a way that let me deploy it with Render every
time there are updates.&lt;/p>
&lt;p>Enter my Rube Goldberg machine.&lt;/p>
&lt;p>The data source I&amp;rsquo;m using, &lt;a href="https://covid-api.com/">covid-api.com&lt;/a>, only updates their data daily,
so I only need to run this once a day to stay relevant. This sounds like a job for cron. But not on
my desktop machine (even though that would be &lt;em>way&lt;/em> simpler).&lt;/p>
&lt;p>Instead, I used &lt;a href="https://github.com/features/actions">GitHub Actions&lt;/a>. It supports scheduled jobs as
well as running on every push to &lt;a href="https://github.com/houseabsolute/local-covid-tracker/">the repo&lt;/a>.
But the trick is to then make the data available after each run. And then the trickier trick is to
get that data as part of running the deploy job on Render. Oh, and every time the GitHub Action
runs, I want to have the Render site deploy again.&lt;/p>
&lt;p>This turned out to be not that hard.&lt;/p>
&lt;p>&lt;a href="https://github.com/houseabsolute/local-covid-tracker/blob/master/.github/workflows/update.yml">My GitHub Actions workflow&lt;/a>
runs the &lt;code>report.pl&lt;/code> script, which generates a &lt;code>summary.json&lt;/code> file. Then the workflow
&lt;a href="https://github.com/marketplace/actions/upload-a-build-artifact">uploads that file as a build artifact&lt;/a>.
This is all incredibly trivial, and by &lt;a href="https://github.com/actions/cache">using caching&lt;/a> for both my
Perl prereqs and the intermediate data files, I can make it quite fast. When the cache is warm, a
run takes less than a minute. When it finishes, it hits a webhook provided by Render to trigger a
deploy.&lt;/p>
&lt;p>Of course, GitHub has
&lt;a href="https://docs.github.com/en/free-pro-team@latest/rest/reference/actions#artifacts">an API for artifacts&lt;/a>
like the &lt;code>summary.json&lt;/code> file. So all I need to do in the Render deploy script is use the API to find
the latest artifact, then download that and deploy it along with my &lt;code>index.html&lt;/code> and &lt;code>chart.js&lt;/code>
files. With a little experimentation, I was able to create a
&lt;a href="https://github.com/houseabsolute/local-covid-tracker/blob/master/deploy.sh">Bash script&lt;/a> to do
exactly that. I could have written this in Perl, but the combination of &lt;code>curl&lt;/code>, &lt;code>jq&lt;/code>, and &lt;code>zcat&lt;/code>
(artifact files are always zipped) actually made this much simpler to do in Bash than Perl&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>. I
had to use &lt;code>sed&lt;/code>, which always seems weird when I know Perl, but doing this in Perl requires at
least a few more characters.&lt;/p>
&lt;p>The hardest part was figuring out how to
&lt;a href="https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets">securely store&lt;/a>
the Render webhook URL in GitHub and then
&lt;a href="https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets">access it in my workflow&lt;/a>.
I had to store a GitHub token in Render&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> as well.&lt;/p>
&lt;p>And so I present to you &lt;a href="https://covid.urth.org/">covid.urth.org&lt;/a>.&lt;/p>
&lt;p>Also, you might note that the chart has changed a little since last time. I made the past 7-day
average line thicker and the daily numbers line thinner. The average is much more indicative of
trends then the actual daily numbers, which jump around quite a bit.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>See my &lt;a href="https://blog.urth.org/2020/09/25/my-move-to-render-is-complete/">previous writeup&lt;/a> on moving all my sites to Render.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>This happens every once in a while.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>I guess I didn&amp;rsquo;t &lt;em>have&lt;/em> to, but the GitHub limit on unauthenticated requests is so low that I
figured it was best to use authentication instead.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>My Local COVID Stats Tracker</title><link>https://blog.urth.org/2020/12/12/my-local-covid-stats-tracker/</link><pubDate>Sat, 12 Dec 2020 14:31:59 -0600</pubDate><guid>https://blog.urth.org/2020/12/12/my-local-covid-stats-tracker/</guid><description>&lt;p>For many months now, I&amp;rsquo;ve been following
&lt;a href="https://www.startribune.com/coronavirus-covid-19-minnesota-tracker-map-county-data/568712601/">the COVID stats in the Star Tribune&lt;/a>,
the local Minneapolis newspaper. There&amp;rsquo;s a lot of interesting info there, but it&amp;rsquo;s not really useful
for reaching conclusions about the safety of various activities. The problem is that the data is
either for the wrong-sized area or I can&amp;rsquo;t group together the bits I care about.&lt;/p>
&lt;p>Most of the stats are state-wide. But I don&amp;rsquo;t care about the whole state. I live in Minneapolis,
part of the Twin Cities metro area. We have more than half of the state&amp;rsquo;s population here. It&amp;rsquo;s the
infection rate in that metro area that really matters to me, as opposed to the whole state. If COVID
is under control in the Iron Range six hours north of me, that has very little impact on how risky
going shopping at the local co-op is.&lt;/p>
&lt;p>They also provide some county-level stats, but there&amp;rsquo;s no way to view a group of counties together
in a historical graph. They also provide
&lt;a href="https://www.startribune.com/minnesota-coronavirus-cases-by-zip-code/572948381/">per-postal code stats&lt;/a>.
This is really useless. For example, one of the postal codes that&amp;rsquo;s most out of control is 56525 on
the west side of the state. Their current case count is a &lt;em>very high&lt;/em> 170 per 1,000 people. Except
that postal code only contains 100 people in total. Insert facepalm here.&lt;/p>
&lt;p>The zip code stats for my local area are arguably less useless, as they contain more than 100 people
each, but it&amp;rsquo;s much too granular. I can&amp;rsquo;t click on dozens of dots and form a mental picture of local
COVID prevalence and trends.&lt;/p>
&lt;p>So I wrote
&lt;a href="https://github.com/houseabsolute/local-covid-tracker">my own hacky tool to do just that&lt;/a>, using
Perl and &lt;a href="https://d3js.org/">&lt;code>d3.js&lt;/code>&lt;/a>. I start by pulling data from
&lt;a href="https://covid-api.com/">covid-api.com&lt;/a>, which allows to me get the daily infection stats by county.
I separate out the four counties I care most about (mine plus a few neighbors). I also have a bucket
for the entire state so I can compare these counties to the state as a whole. There is one line for
each of these, lightly smoothed. Then it adds a thinner past 7-day average line for each of these as
well, so I can get a better sense of the trends. I wish that this API provided infection rates per
1,000 people, but I can still see trends just by looking at daily new infections.&lt;/p>
&lt;p>I save all this in a JSON file, and then use &lt;code>d3.js&lt;/code> to make a reasonably pretty chart. The API
calls are cached so I don&amp;rsquo;t beat up the server. Whenever I run the script it&amp;rsquo;ll download any missing
days of data.&lt;/p>
&lt;p>And here&amp;rsquo;s the end result:&lt;/p>
&lt;link rel="stylesheet" href="https://blog.urth.org/css/hugo-easy-gallery.css" />
&lt;div class="box">
&lt;figure itemprop="associatedMedia"
itemscope itemtype="http://schema.org/ImageObject" >
&lt;div class="img">
&lt;img itemprop="thumbnail" src="https://blog.urth.org/image/2020-12-12-covid-chart.png" alt="A snapshot of one of my charts local COVID trends"/>
&lt;/div>
&lt;a href="https://blog.urth.org/image/2020-12-12-covid-chart.png" itemprop="contentUrl">&lt;/a>
&lt;/figure>
&lt;/div>
&lt;p>The d3 code is super cargo culted, so I&amp;rsquo;m sure it&amp;rsquo;s terrible.&lt;/p>
&lt;p>The &lt;a href="https://github.com/houseabsolute/local-covid-tracker">code is on GitHub&lt;/a>, of course. I think
it&amp;rsquo;s usable for other people, though PRs to make it better are welcome. At some point I may try to
add a mouseover handler to show the values for each date, like in
&lt;a href="https://bl.ocks.org/larsenmtl/e3b8b7c2ca4787f77d78f58d41c3da91">this example&lt;/a>.&lt;/p></description></item></channel></rss>