<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://bridgetownrb.com/" version="2.1.1">Bridgetown</generator><link href="https://katafrakt.me/feed.xml" rel="self" type="application/atom+xml" /><link href="https://katafrakt.me/" rel="alternate" type="text/html" /><updated>2026-05-29T20:12:04+02:00</updated><id>https://katafrakt.me/feed.xml</id><title type="html">katafrakt’s garden</title><subtitle>Just my website. Some Elixir, some Ruby, some more exotic languages and general thoughts on tech.</subtitle><entry><title type="html">What if Hanami had templateless views?</title><link href="https://katafrakt.me/2026/05/29/hanami-templateless-views/" rel="alternate" type="text/html" title="What if Hanami had templateless views?" /><published>2026-05-29T04:01:29+02:00</published><updated>2026-05-29T04:01:29+02:00</updated><id>repo://posts.collection/_posts/2026-05-29-hanami-templateless-views.md</id><content type="html" xml:base="https://katafrakt.me/2026/05/29/hanami-templateless-views/">&lt;p&gt;In the new glorous website of &lt;a href=&quot;https://hanakai.org&quot;&gt;Hanakai&lt;/a&gt; there are two “getting started” guides for Hanami: one for a “web app” (meaning, a fullstack app) and one for an API app. Even though those two guides follow building the same application (bookshelf), they differ quite a bit in places. Sometimes it’s absolutely justified (you don’t have HTML templates in API app), sometimes it feels justified, but can also provoke a thought. This is one of these thoughts.&lt;/p&gt;

&lt;p&gt;Let’s look about 30% in the guide at “Fetching books from database” subsection. It demonstrates how to get a books index page, paginated. This is how is looks for a fullstack app (combined into one listing for clarity):&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# action&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Bookshelf&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Actions&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Home&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Index&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Bookshelf&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Action&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# view&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Bookshelf&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Views&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Books&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Index&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Bookshelf&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;View&lt;/span&gt;
        &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Deps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;repos.book_repo&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;expose&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:books&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;book_repo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;all_by_title&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# template&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;h1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Books&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/h1&amp;gt;

&amp;lt;ul&amp;gt;
  &amp;lt;% books.each do |book| %&amp;gt;
    &amp;lt;li&amp;gt;&amp;lt;%= book[:title] %&amp;gt;, by &amp;lt;%= book[:author] %&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;li&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;% end &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/ul&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;… and this is for API:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Bookshelf&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Actions&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Books&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Index&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Bookshelf&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Action&lt;/span&gt;
        &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Deps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;repos.book_repo&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;books&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;book_repo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;all_by_title&lt;/span&gt;

          &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;format&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:json&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;books&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:to_h&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_json&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I looked at these code listing probably dozens of times, until it hit me:&lt;br /&gt;
Why are actions &lt;strong&gt;so much different&lt;/strong&gt; here?&lt;/p&gt;

&lt;p&gt;The fullstack app action is empty. It does nothing. Okay, this is not true, it actually does &lt;em&gt;something&lt;/em&gt;, but it’s hidden by the framework mechanics, but it does more or less that:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;format&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:html&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render_the_view_somehow&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It looks more similar to the API version now, but there is one jarring difference now: the API action needs to know about a repository and how to call it. Or does it actually &lt;em&gt;need&lt;/em&gt; that? The instinctive answer is: yes. We don’t render HTML so we don’t have the view layer, so there’s no other choice, it needs to go to the controller! This is we built JSON APIs in Rails and Sinatra for decades. Now as a long-time Phoenix user and its dead-view&lt;label for=&quot;b81f01de-d6f0-4a2d-8c17-246589e0d857&quot; class=&quot;sidenote-toggle sidenote-number&quot;&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;b81f01de-d6f0-4a2d-8c17-246589e0d857&quot; class=&quot;sidenote-toggle&quot; /&gt;&lt;span class=&quot;sidenote&quot;&gt;For those unfamiliar, “dead views” is a term coined in opposition to LiveView - in Ruby terms it’s just a regular server-rendered view.&lt;/span&gt; enjoyer, I dare to challenge that.&lt;/p&gt;

&lt;p&gt;Without further ado, I think it could look like this:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# action&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Bookshelf&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Actions&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Books&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Index&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Bookshelf&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Action&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;format&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:json&lt;/span&gt; 
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# view&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Bookshelf&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Views&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Books&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Index&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Bookshelf&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;View&lt;/span&gt;
        &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Deps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;repos.book_repo&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;expose&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:books&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;book_repo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;all_by_title&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
        
        &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;books: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;books&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:to_h&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This way we keep original separation of concerns from the fullstack Hanami app - the action deals with request/response stuff, while the view is responsible for the shape and content of the response. In a large app both of them will grow, but they will grow independently. When we add permissions to see books index page, only action will change, indicating that the format of the response remains unchanged. Similarly, when we need to change the structure of the JSON, the change will be local to the view class.&lt;/p&gt;

&lt;h2 id=&quot;implemetation&quot;&gt;Implemetation&lt;/h2&gt;

&lt;p&gt;Currently Hanami relies on Tilt and file-based templates to render views. In order to achieve what I described above, we would need to define custom &lt;code class=&quot;highlighter-rouge&quot;&gt;Renderer&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;Rendering&lt;/code&gt; classes:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;InlineRenderer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Hanami&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;View&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Renderer&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;initialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render_proc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@render_proc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render_proc&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@template&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;instance_exec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@render_proc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_json&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;InlineRendering&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Hanami&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;View&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Rendering&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;initialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render_proc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@renderer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;InlineRenderer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render_proc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The we use it in our application’s &lt;code class=&quot;highlighter-rouge&quot;&gt;View&lt;/code&gt; class:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Bookshelf&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Hanami&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;View&lt;/span&gt;
    &lt;span class=&quot;kp&quot;&gt;extend&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Dry&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Core&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ClassAttributes&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;defines&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:render_proc&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rendering&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;format: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;default_format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;context: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;default_context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;InlineRendering&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;config: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;format: &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;context: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;render_proc: &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;render_proc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;render_proc&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can see it in action in &lt;a href=&quot;https://codeberg.org/katafrakt/bookshelf_api&quot;&gt;this repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;the-doors-it-opens&quot;&gt;The doors it opens&lt;/h2&gt;

&lt;p&gt;Just to be clear, what I show here is the result of my experimentation, not a sneak peek into what Hanami will offer soon. This experiment was, however, quite important to me.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Even though currently Hanami is kind of coupled to Tilt and its HTML templates, it’s possible to change that in about 20 lines of code.&lt;/li&gt;
  &lt;li&gt;It opens up possibility to integrate Phlex into &lt;code class=&quot;highlighter-rouge&quot;&gt;hanami-view&lt;/code&gt;, not replacing it&lt;/li&gt;
  &lt;li&gt;Many people think JSON responses are simple compared to HTML ones. For sure they tend to be more focused, but they definitely can grow a lot. Having the whole view layer at your disposal means that API application can have this pain eased by Hanami as much as fullstack apps.&lt;/li&gt;
&lt;/ul&gt;</content><author><name></name></author><category term="ruby" /><category term="hanami" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://katafrakt.me/assets/og-gen/hanami-templateless-views.png" /><media:content medium="image" url="https://katafrakt.me/assets/og-gen/hanami-templateless-views.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Untimely feedback as a root cause of tech debt</title><link href="https://katafrakt.me/2026/05/12/untimely-feedback/" rel="alternate" type="text/html" title="Untimely feedback as a root cause of tech debt" /><published>2026-05-12T12:50:10+02:00</published><updated>2026-05-12T12:50:10+02:00</updated><id>repo://posts.collection/_posts/2026-05-12-untimely-feedback.md</id><content type="html" xml:base="https://katafrakt.me/2026/05/12/untimely-feedback/">&lt;p&gt;Have you ever worked on a brand new codebase with talented engineers, only to find out a year later that it turned into the same “archaeological strata of quick fixes” as your last legacy project? I know I have, and it happened many times.&lt;/p&gt;

&lt;p&gt;My go-to blame target for this was usually micromanagement and imposed unrealistic deadlines. And I’m sure these play a significant role in it. But a recent conversation led me to understand another cause of this – untimely feedback. Or, to be precise, feedback that was requested or given too late.&lt;/p&gt;

&lt;p&gt;I think many of us know the story. Someone starts working on a new capability. They spin off a feature branch and keep tinkering with it for a couple of days. Then they submit a PR. Thousands of lines, large diffs, the feeling of being overwhelmed and then the despair when you find a fundamental flaw in the design. The one that is not disqualifying, but one that would require a complete rewrite to do the thing correctly.&lt;/p&gt;

&lt;p&gt;From there, there are two options. And neither of them is good.&lt;/p&gt;

&lt;p&gt;First, you can point that out and request changes. This is also known as “being &lt;em&gt;that person&lt;/em&gt;”. Nobody wants to have this label. Plus it frequently results in a heated back-and-forth when the author desperately wants to avoid spending another week on a full rewrite.&lt;/p&gt;

&lt;p&gt;The other option is to let that pass. You reluctantly approve the PR and the code lands in the trunk. You know it will cause trouble in the future and it inevitably does. The codebase got worse, the tech debt increased. But you don’t want to be &lt;em&gt;that person&lt;/em&gt; and don’t want to have “the talk” with your manager about The Velocity.&lt;/p&gt;

&lt;p&gt;You might think that this could have been avoided if the author asked for feedback first. But why didn’t they? Is it because they like to work this &lt;em&gt;fait accompli&lt;/em&gt; way? Most likely not. In fact, maybe they did ask for early feedback but nobody provided it.&lt;/p&gt;

&lt;p&gt;This also happens a lot. Someone writes a high-level description of a solution and… no one bothers. It would require taking this description and translating it yourself into something more tangible. That’s the way to spot a problem from a high-level description. It constitutes an effort, often much bigger than just glossing over a ready-to-review solution. This creates a cycle that is hard to break:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;People don’t ask for early feedback, because they don’t think they’ll receive it&lt;/li&gt;
  &lt;li&gt;Late feedback is not given, because it’s too late&lt;/li&gt;
  &lt;li&gt;Code with design flaws gets merged, not because of someone’s bad faith or inability to write a good one, but because two pairs of eyes are usually better than one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My takeaway from that is to ask for early feedback more often. Even if I don’t believe in receiving it, just creating a possibility of it might pay off in the future. And to try to push myself to give early feedback whenever it’s viable, even if I don’t feel like making this mental effort. As a side benefit, this would also make the development process more collaboration-based than sign-off-based, which is definitely a win.&lt;/p&gt;</content><author><name></name></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://katafrakt.me/assets/og-gen/untimely-feedback.png" /><media:content medium="image" url="https://katafrakt.me/assets/og-gen/untimely-feedback.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Portable mruby binaries with Cosmopolitan</title><link href="https://katafrakt.me/2026/01/04/mruby-cosmo-compilation/" rel="alternate" type="text/html" title="Portable mruby binaries with Cosmopolitan" /><published>2026-01-04T14:16:49+01:00</published><updated>2026-01-04T14:16:49+01:00</updated><id>repo://posts.collection/_posts/2026-01-03-mruby-cosmo-compilation.md</id><content type="html" xml:base="https://katafrakt.me/2026/01/04/mruby-cosmo-compilation/">&lt;p&gt;One of my main interests with mruby is its ability to create standalone executables. As explored in &lt;a href=&quot;/2024/10/05/mruby-beyond-hello-world/&quot;&gt;this post&lt;/a&gt;, this is absolutely possible and works well. However, there is a classic issue of most standalone binaries: they can only be run on the same, or very similar, system as the one they were built on.&lt;/p&gt;

&lt;p&gt;For example, if I take an executable from my &lt;a href=&quot;https://github.com/katafrakt/mruby-hello-world&quot;&gt;mruby Hello World project&lt;/a&gt;, built on amd64 Linux, it won’t work on Silicon Mac:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ ./hello
exec: Failed to execute process: &apos;./hello&apos; the file could not be run by the operating system.
exec: Maybe the interpreter directive (#! line) is broken?
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Mruby &lt;strong&gt;does&lt;/strong&gt; have some &lt;a href=&quot;https://github.com/mruby/mruby/blob/master/doc/guides/compile.md#cross-compilation-1&quot;&gt;cross-compilation capabilities&lt;/a&gt;, but I’ll be honest: it always felt intimidating, complected and I never actually got it working from Linux to MacOS. So, are we doomed here? Is it possible to build standalone binaries for MacOS without a MacOS docker image or something?&lt;/p&gt;

&lt;p&gt;While reading &lt;a href=&quot;https://github.com/mruby/mruby/blob/master/NEWS.md&quot;&gt;the changelog&lt;/a&gt; of mruby I noticed one entry there that got me curious:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;New Platform: Cosmopolitan Libc&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I never heard about Cosmopolitan, but a quick search revealed that it is what might address the issue mentioned above. From &lt;a href=&quot;https://justine.lol/cosmopolitan/index.html&quot;&gt;the project’s website&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Cosmopolitan Libc makes C a build-anywhere run-anywhere language, like Java, except it doesn’t need an interpreter or virtual machine. Instead, it reconfigures stock GCC and Clang to output a POSIX-approved polyglot format that runs natively on Linux + Mac + Windows + FreeBSD + OpenBSD + NetBSD + BIOS on AMD64 and ARM64 with the best possible performance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Alright, let’s give it a try then!&lt;/p&gt;

&lt;h2 id=&quot;building-an-mruby-project-with-cosmopolitan&quot;&gt;Building an mruby project with Cosmopolitan&lt;/h2&gt;

&lt;p&gt;The regular steps to build a standalone binary are as shown in the &lt;a href=&quot;https://github.com/katafrakt/mruby-hello-world/blob/808d8c8786bf6e9340d2da6d82dc689349548cbf/Rakefile&quot;&gt;Rakefile&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Build mruby&lt;/li&gt;
  &lt;li&gt;Generate bytecode from the Ruby code, using &lt;code class=&quot;highlighter-rouge&quot;&gt;mrbc&lt;/code&gt; built in step 1&lt;/li&gt;
  &lt;li&gt;Compile the binary using a C entry file and the bytecode generated in step 2&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For Cosmopolitan, it’s simple, but we need some initial setup.&lt;/p&gt;

&lt;p&gt;First, let’s download the Cosmopolitan toolkit from &lt;a href=&quot;https://cosmo.zip/pub/cosmocc/&quot;&gt;https://cosmo.zip/pub/cosmocc/&lt;/a&gt;. I extracted it to &lt;code class=&quot;highlighter-rouge&quot;&gt;~/Downloads/cosmocc&lt;/code&gt; for the purpose of this article.&lt;/p&gt;

&lt;p&gt;Then we can build mruby using it:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;COSMO_ROOT=~/Downloads/cosmocc rake MRUBY_CONFIG=cosmopolitan
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;callout callout-info&quot;&gt;
&lt;p&gt;At the time of writing, I needed to remove mirb (&lt;a href=&quot;https://github.com/mruby/mruby/blob/c10a9ec571d43da8a883c5d7bfbbe61ab05457f9/build_config/cosmopolitan.rb#L84&quot;&gt;this line&lt;/a&gt;) from the config, because it failed to compile with Cosmopolitan.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;Then we can generate the bytecode. Note the different executable name:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;./mruby/bin/mrbc.com -Bhello_world hello.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Finally, compile the entry file:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;~/Downloads/cosmocc/bin/cosmocc -std=c99 -Imruby/include main.c -o hello mruby/build/host/lib/libmruby.a -lm
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After that we get a binary &lt;code class=&quot;highlighter-rouge&quot;&gt;hello&lt;/code&gt;. And if I copy it over to my Mac, it just works there! The binary size is larger, of course, which is the understandable price. But if it is fine for the project you are working on, for sure this is a simpler way than providing multiple binaries targetting different platforms.&lt;/p&gt;</content><author><name></name></author><category term="mruby" /><category term="ruby" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://katafrakt.me/assets/og-gen/mruby-cosmo-compilation.png" /><media:content medium="image" url="https://katafrakt.me/assets/og-gen/mruby-cosmo-compilation.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Using Elixir head version with Mise</title><link href="https://katafrakt.me/2026/01/03/elixir-head-with-mise/" rel="alternate" type="text/html" title="Using Elixir head version with Mise" /><published>2026-01-03T03:00:00+01:00</published><updated>2026-01-03T03:00:00+01:00</updated><id>repo://posts.collection/_posts/2026-01-03-elixir-head-with-mise.md</id><content type="html" xml:base="https://katafrakt.me/2026/01/03/elixir-head-with-mise/">&lt;p&gt;In 2025 I followed the direction many people headed in and switched my version manager to &lt;a href=&quot;https://mise.jdx.dev&quot;&gt;mise-en-place&lt;/a&gt;. It has been going really well for me, but recently I came across a problem which took me a bit to figure out (and few &lt;em&gt;very&lt;/em&gt; frustrating conversations with LLM, which did not lead to anything). I wanted to test some code with unreleased version 1.20 of Elixir, straight from git.&lt;/p&gt;

&lt;p&gt;How to do it? It’s really simple in retrospect. Here are the steps:&lt;/p&gt;

&lt;h3 id=&quot;1-build-elixir&quot;&gt;1. Build Elixir&lt;/h3&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ git clone https://github.com/elixir-lang/elixir.git 
$ cd elixir
$ make
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Verify it’s built correctly:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ bin/elixir --version
&amp;gt; Elixir 1.20.0-dev (88cbabf) (compiled with Erlang/OTP 28)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;2-link-it-in-mise&quot;&gt;2. Link it in mise&lt;/h3&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mise link elixir@head /path/to/elixir
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;3-verify-this-works&quot;&gt;3. Verify this works&lt;/h3&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ mise x elixir@head -- elixir --version
&amp;gt; Elixir 1.20.0-dev (88cbabf) (compiled with Erlang/OTP 28)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And it’s done. Whenever you want to run something with Elixir head, you can just &lt;code class=&quot;highlighter-rouge&quot;&gt;mise use elixir@head&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Of course, this should work in a similar way for everything that is handles by mise version control.&lt;/p&gt;</content><author><name></name></author><category term="elixir" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://katafrakt.me/assets/og-gen/elixir-head-with-mise.png" /><media:content medium="image" url="https://katafrakt.me/assets/og-gen/elixir-head-with-mise.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Integrating Pagy with Hanami (2025 edition)</title><link href="https://katafrakt.me/2025/11/06/integrating-pagy-with-hanami-2025/" rel="alternate" type="text/html" title="Integrating Pagy with Hanami (2025 edition)" /><published>2025-11-06T11:00:00+01:00</published><updated>2025-11-06T11:00:00+01:00</updated><id>repo://posts.collection/_posts/2025-11-06-integrating-pagy-with-hanami-2025.md</id><content type="html" xml:base="https://katafrakt.me/2025/11/06/integrating-pagy-with-hanami-2025/">&lt;p&gt;Back in 2018 &lt;a href=&quot;/2018/06/01/integrating-pagy-with-hanami/&quot;&gt;I wrote a post&lt;/a&gt; about connecting Hanami (then 1.x) and Pagy gem together. My verdict was not that favorable. I had to write quite a lot of glue code to make it work and perhaps the worst thing was that I had to pass the &lt;code class=&quot;highlighter-rouge&quot;&gt;request&lt;/code&gt; object to the template to make it work.&lt;/p&gt;

&lt;p&gt;However, things have changed since then:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Hanami is now 2.3, its persistence layer is more mature, based on ROM and allows to fall back to almost-bare-Sequel via relations&lt;label for=&quot;f61a21bd-00f7-40ca-b28b-a413dfb84f99&quot; class=&quot;sidenote-toggle sidenote-number&quot;&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;f61a21bd-00f7-40ca-b28b-a413dfb84f99&quot; class=&quot;sidenote-toggle&quot; /&gt;&lt;span class=&quot;sidenote&quot;&gt;Looking back, perhaps it was already possible in 2018, but I took the wrong approach? Hard to tell now. I don’t want to go back to these old versions to verify.&lt;/span&gt;.&lt;/li&gt;
  &lt;li&gt;Pagy &lt;a href=&quot;https://github.com/ddnexus/pagy/releases/tag/43.0.0&quot;&gt;released version 43&lt;/a&gt; this week. It’s advertised as a complete rewrite of its internals and APIs.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
  &lt;p&gt;We needed a leap version to unequivocally signaling that it’s not just a major version: it’s a complete redesign of the legacy code at all levels, usage and API included.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There’s no better time to take it for another spin then!&lt;/p&gt;

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

&lt;p&gt;I quickly spun up a fresh Hanami app, created a migration:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;ROM&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SQL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;migration&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;change&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;create_table&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:people&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;primary_key&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;column&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;column&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;column&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:birth_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;column&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:added_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;DateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then created a relation:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Pagynation&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Relations&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;People&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Hanami&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Relation&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;schema&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:people&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;infer: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next I generated the action and added Pagy to it.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;pagy&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Pagynation&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Actions&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;People&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Pagynation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Action&lt;/span&gt;
        &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Deps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;relations.people&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Pagy&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Method&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;records&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;people&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;client_max_limit: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;40&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;view&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;pagy: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;people: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;records&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This looks more or less like the tutorial code from the Pagy website. The only exception is that I have to pass the &lt;code class=&quot;highlighter-rouge&quot;&gt;request&lt;/code&gt; directy. If not passed, Pagy expects &lt;code class=&quot;highlighter-rouge&quot;&gt;self.request&lt;/code&gt; to be defined. This does not work with Hanami, where the request is passed as an argument (in a functional flavour), not burned into the controller instance.&lt;/p&gt;

&lt;p&gt;Let’s now examine the view:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Pagynation&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Views&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;People&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Pagynation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;View&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;expose&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:people&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;decorate: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;expose&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:paginator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;decorate: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pagy&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:|&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;pagy&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is standard Hanami stuff. We passed &lt;code class=&quot;highlighter-rouge&quot;&gt;:pagy&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;:people&lt;/code&gt; from the action via the &lt;code class=&quot;highlighter-rouge&quot;&gt;render&lt;/code&gt; method, now we need to pass it on to the template. I used &lt;code class=&quot;highlighter-rouge&quot;&gt;decorate: false&lt;/code&gt; here to keep the things as simple as possible (data is a simple hash, not objects). With &lt;code class=&quot;highlighter-rouge&quot;&gt;people&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;paginator&lt;/code&gt; exposed, all that’s left is to craft a template using it.&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;People list&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;table&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;thead&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;th&amp;gt;&lt;/span&gt;Name&lt;span class=&quot;nt&quot;&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;th&amp;gt;&lt;/span&gt;Email&lt;span class=&quot;nt&quot;&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;th&amp;gt;&lt;/span&gt;Date of Birth&lt;span class=&quot;nt&quot;&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/thead&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;tbody&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;people.each&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;person&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;person&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;person&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;person&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;:birth_date&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;raw&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;paginator.info_tag&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;raw&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;paginator.series_nav&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;paginator.info_tag&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;paginator.series_nav&lt;/code&gt; are Pagy’s HTML helper to render some status info and pagination links. They are, of course, quite barebones, but more than enough to test stuff.&lt;/p&gt;

&lt;p&gt;This all led me to this beauty, with working &lt;code class=&quot;highlighter-rouge&quot;&gt;limit&lt;/code&gt; URL params, working links etc.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/pagy-43-hanami.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;new-verdict&quot;&gt;New verdict&lt;/h2&gt;

&lt;p&gt;This is basically great.&lt;/p&gt;

&lt;p&gt;Everything worked pretty much out-of-the-box. If I were to imagine how seamless a pagination library for Hanami could be, this would likely look like that. Great improvements from Pagy maintainer.&lt;/p&gt;

&lt;p&gt;The repository to try it out yourself is available here &lt;a href=&quot;https://codeberg.org/katafrakt/hanami-pagy&quot;&gt;on Codeberg&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy paginating!&lt;/p&gt;</content><author><name></name></author><category term="ruby" /><category term="hanami" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://katafrakt.me/assets/og-gen/integrating-pagy-with-hanami-2025.png" /><media:content medium="image" url="https://katafrakt.me/assets/og-gen/integrating-pagy-with-hanami-2025.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Eglot, Ruby LSP and StandardRB</title><link href="https://katafrakt.me/2025/11/04/eglot-ruby-lsp-standardrb/" rel="alternate" type="text/html" title="Eglot, Ruby LSP and StandardRB" /><published>2025-11-04T00:01:00+01:00</published><updated>2025-11-04T00:01:00+01:00</updated><id>repo://posts.collection/_posts/2025-11-04-eglot-ruby-lsp-standardrb.md</id><content type="html" xml:base="https://katafrakt.me/2025/11/04/eglot-ruby-lsp-standardrb/">&lt;p&gt;I use &lt;a href=&quot;https://github.com/doomemacs/doomemacs&quot;&gt;Doom Emacs&lt;/a&gt; as my main coding editor, and &lt;code class=&quot;highlighter-rouge&quot;&gt;eglot&lt;/code&gt; for language server shenanigans. My config is mainly optimized towards Elixir, so for Ruby I was mostly using the default Doom’s Ruby module. It worked pretty well for me.&lt;/p&gt;

&lt;p&gt;The main hurdle was always projects not using Rubocop. I prefer &lt;a href=&quot;https://github.com/standardrb/standard&quot;&gt;StandardRB&lt;/a&gt;, because I don’t like bike-shedding, and their defaults are really good for me. But in the absence of &lt;code class=&quot;highlighter-rouge&quot;&gt;.rubocop.yml&lt;/code&gt; file, Doom tried to use default Rubocop settings for linting, instead of detecting StandardRB. In the past, I worked around this by replacing &lt;code class=&quot;highlighter-rouge&quot;&gt;rubocop-mode&lt;/code&gt; with &lt;code class=&quot;highlighter-rouge&quot;&gt;standard-mode&lt;/code&gt;, but recently Doom’s maintainer decided to go full-on with Ruby LSP for formatting and linting, so I had to re-hack it.&lt;/p&gt;

&lt;p&gt;Fortunately, Ruby LSP supports addons and &lt;a href=&quot;https://github.com/standardrb/standard/blob/main/lib/ruby_lsp/standard/addon.rb&quot;&gt;there is one&lt;/a&gt; for StandardRB. Unfortunately, it’s not as easy as it sounds…&lt;/p&gt;

&lt;h2 id=&quot;tldr&quot;&gt;tl;dr&lt;/h2&gt;

&lt;p&gt;You need to put this in your &lt;code class=&quot;highlighter-rouge&quot;&gt;.dir-locals.el&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-elisp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;ruby-mode&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;eglot-server-programs&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;ruby-mode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;ruby-lsp&quot;&lt;/span&gt;
                       &lt;span class=&quot;ss&quot;&gt;:initializationOptions&lt;/span&gt;
                       &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:formatter&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;standard&quot;&lt;/span&gt;
                        &lt;span class=&quot;ss&quot;&gt;:linters&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;standard&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;]&lt;/span&gt;
                        &lt;span class=&quot;ss&quot;&gt;:enabledFeatures&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:codeActions&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;t&lt;/span&gt;
                                          &lt;span class=&quot;ss&quot;&gt;:diagnostics&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;t&lt;/span&gt;
                                          &lt;span class=&quot;ss&quot;&gt;:formatting&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))))))))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;how-i-got-there&quot;&gt;How I got there&lt;/h2&gt;

&lt;p&gt;Using &lt;code class=&quot;highlighter-rouge&quot;&gt;.dir-locals.el&lt;/code&gt; is what &lt;code class=&quot;highlighter-rouge&quot;&gt;eglot&lt;/code&gt; &lt;a href=&quot;https://www.gnu.org/software/emacs/manual/html_node/eglot/Project_002dspecific-configuration.html&quot;&gt;suggests in its documentation&lt;/a&gt;. However, the suggested form did not work for me:&lt;/p&gt;

&lt;div class=&quot;language-elisp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
   &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;eglot-workspace-configuration&lt;/span&gt;
       &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:rubyLsp&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:formatter&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;standard&quot;&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:linters&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;standard&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;]&lt;/span&gt;
           &lt;span class=&quot;ss&quot;&gt;:enabledFeatures&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:codeActions&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:diagnostics&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:formatting&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))))))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After examining the &lt;code class=&quot;highlighter-rouge&quot;&gt;eglot-events-buffer&lt;/code&gt;, I realized that &lt;code class=&quot;highlighter-rouge&quot;&gt;eglot&lt;/code&gt; was sending &lt;code class=&quot;highlighter-rouge&quot;&gt;workspace/didChangeConfiguration&lt;/code&gt; event with my new config, including my chosen formatter and linter. But apparently, Ruby LSP server was not doing anything with that message.&lt;/p&gt;

&lt;p&gt;Indeed, after going through the code, I found that &lt;code class=&quot;highlighter-rouge&quot;&gt;workspace/didChangeConfiguration&lt;/code&gt; is not a supported message by Ruby LSP! I &lt;a href=&quot;https://github.com/Shopify/ruby-lsp/issues/3785&quot;&gt;offered to add it&lt;/a&gt; and spent few days on &lt;a href=&quot;https://github.com/Shopify/ruby-lsp/pull/3813&quot;&gt;implementation&lt;/a&gt;, only to find out that it would not help me. Why?&lt;/p&gt;

&lt;p&gt;The issue here is that, upon initializing the language server connection, Ruby LSP checks for supported formatters and finds nothing. After that it, sets &lt;code class=&quot;highlighter-rouge&quot;&gt;documentFormattingProvider&lt;/code&gt; to &lt;code class=&quot;highlighter-rouge&quot;&gt;null&lt;/code&gt;, effectively telling the client that it does not support formatting. Period. &lt;code class=&quot;highlighter-rouge&quot;&gt;eglot&lt;/code&gt; notes it down and whenever the user requests formatting, it just ignores the request, knowing it won’t be fulfilled.&lt;/p&gt;

&lt;p&gt;So, the next task after implementing support for &lt;code class=&quot;highlighter-rouge&quot;&gt;workspace/didChangeConfiguration&lt;/code&gt; would be to add server dynamically registering new capabilities, formatter in this case. While it sounded promising and after I did most of the work, I unfortunately realized that &lt;code class=&quot;highlighter-rouge&quot;&gt;eglot&lt;/code&gt; does not support dynamic registrations :( I scraped the code and looked for another solution…&lt;/p&gt;

&lt;p&gt;… which, in this case, is to simply shut down the LS connection and reinitialize it with &lt;code class=&quot;highlighter-rouge&quot;&gt;standard&lt;/code&gt; formatter and &lt;code class=&quot;highlighter-rouge&quot;&gt;linter&lt;/code&gt; from the start. At first I wasn’t really happy about it, but then I figured this is actually simple and efficient solution, much in line with Erlang’s philosophy of killing and restarting things, rather than using elaborate reconfiguration or healing techniques.&lt;/p&gt;

&lt;p&gt;In any case, I now have a working formatter and linter and know a lot more about language servers.&lt;/p&gt;

&lt;p&gt;Next stop: making StandardRB support region formatting (Rubocop does not support it either).&lt;/p&gt;</content><author><name></name></author><category term="ruby" /><category term="emacs" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://katafrakt.me/assets/og-gen/eglot-ruby-lsp-standardrb.png" /><media:content medium="image" url="https://katafrakt.me/assets/og-gen/eglot-ruby-lsp-standardrb.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">My OCaml-flavoured Elixir style</title><link href="https://katafrakt.me/2025/09/23/ocaml-flavoured-elixir/" rel="alternate" type="text/html" title="My OCaml-flavoured Elixir style" /><published>2025-09-23T00:01:00+02:00</published><updated>2025-09-23T00:01:00+02:00</updated><id>repo://posts.collection/_posts/2025-09-23-ocaml-flavoured-elixir.md</id><content type="html" xml:base="https://katafrakt.me/2025/09/23/ocaml-flavoured-elixir/">&lt;p&gt;Recently I’m finding myself leaning towards writing some Elixir code in a bit different way than the community standard. I call it, perhaps unjustly and a bit tongue-in-cheek, “OCaml-flavoured Elixir”. Now, I don’t really write OCaml well (or: at all), but I spent last 3 years working with a frontend written in &lt;a href=&quot;https://rescript-lang.org/&quot;&gt;ReScript&lt;/a&gt;. And I think in recent months it started to affect how I think about the Elixir code.&lt;/p&gt;

&lt;p&gt;But to start the conversation, let me show you what I actually mean:&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;close_ticket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ticket_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actor_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;fetch_ticket&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Ticket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ticket_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wrap_not_nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:ticket_not_found&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;fetch_user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actor_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wrap_not_nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:user_not_found&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ticket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fetch_ticket&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fetch_user&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;:ok&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;TicketPolicy&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;can_close?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ticket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;Tickets&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ticket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from_struct&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&amp;amp;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:closed_by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I see few eyebrows raised, perhaps even some pitchforks picked up, but allow me explain why I find this style of code easier to follow.&lt;/p&gt;

&lt;h2 id=&quot;1-self-containing&quot;&gt;1. Self-containing&lt;/h2&gt;

&lt;p&gt;Traditional rules of writing Elixir code, stemming from Ruby, stemming probably from Java (?), would urge us to create private functions &lt;code class=&quot;highlighter-rouge&quot;&gt;fetch_ticket&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;fetch_users&lt;/code&gt;, wrapping database calls in a result tuples (&lt;code class=&quot;highlighter-rouge&quot;&gt;{:ok, user}&lt;/code&gt;). This is an understandable coding pattern, however it leads to “jumping around” the codebase. The private function will usually be defined below the public function using it, perhaps even at the end of a file. This leads to very “choppy” experience when reading the code.&lt;/p&gt;

&lt;p&gt;Another problem is creating non-reusable private function, which serve only to encapsulate some code logic. They often receive a lot of arguments, same as the private function calling them. These, in my opinion, are signals of suboptimal design.&lt;/p&gt;

&lt;p&gt;Defining anonymous functions inside the public function changes the reading dynamic a bit. When you read a function, you read it top-down. This means that upon getting to the &lt;code class=&quot;highlighter-rouge&quot;&gt;with&lt;/code&gt; piipeline, you have at least skimmed over the &lt;code class=&quot;highlighter-rouge&quot;&gt;fetch_ticket&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;fetch_user&lt;/code&gt;. If you want to go back to read what they do, they are here, at hand, not &lt;em&gt;somewhere in the module&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Of course, this might get out of hand with multiple complex anonymous functions defined. I try to keep 3 of them at most, otherwise it obfuscates where the actual function “meat” begins.&lt;/p&gt;

&lt;h2 id=&quot;2-follows-prepare-execute-format-flow&quot;&gt;2. Follows prepare-execute-format flow&lt;/h2&gt;

&lt;p&gt;Majority of &lt;em&gt;code actions&lt;/em&gt;, especially the ones modeling business actions, follow a certain flow. First you prepare data, then you execute an action, then - optionally - you format the result. Here defining a functions serve as a preparation. The &lt;code class=&quot;highlighter-rouge&quot;&gt;with&lt;/code&gt; pipeline is execution and we have some lightweight formatting as a map too. I could just take this code, print it, and then draw the line where the “preparation” step finishes and execution begins.&lt;/p&gt;

&lt;p&gt;With private functions instead, I would have the preparation conflated with execution. It’s not bad, but it’s less explicit.&lt;/p&gt;

&lt;h2 id=&quot;3-using-ocaml-like-helpers&quot;&gt;3. Using OCaml-like helpers&lt;/h2&gt;

&lt;p&gt;If you are wondering what &lt;code class=&quot;highlighter-rouge&quot;&gt;Result.map&lt;/code&gt; is, it comes from my &lt;a href=&quot;https://hexdocs.pm/fey/readme.html&quot;&gt;Fey&lt;/a&gt; library. I created and published it some time ago, but never really announce it. It contains multiple functions to work with result tuples (and introduces option tuples), inspired by OCaml/ReScript.&lt;/p&gt;

&lt;p&gt;In the case above, it allows to simplify code that would often be a case statement:&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Tickets&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ticket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;:ok&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;ticket&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from_struct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:closed_by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&amp;amp;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;My codebase is full of snippets like this. If the result is &lt;code class=&quot;highlighter-rouge&quot;&gt;{:ok, something}&lt;/code&gt; then do some small transformation of &lt;code class=&quot;highlighter-rouge&quot;&gt;something&lt;/code&gt; and wrap it back with &lt;code class=&quot;highlighter-rouge&quot;&gt;{:ok, _}&lt;/code&gt;. If not, just pass the result through.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;Fey.Result.map&lt;/code&gt; wraps it with a convenient helper, just as OCaml does. This is much more terse and concentrating on what’s important - modifying the result value.&lt;/p&gt;

&lt;p&gt;Similarily, the &lt;code class=&quot;highlighter-rouge&quot;&gt;Fey.Result.wrap_not_nil&lt;/code&gt; helps avoid repetitive boilerplate of&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Ticket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:ticket_not_found&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;ticket&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ticket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;thats-it&quot;&gt;That’s it&lt;/h2&gt;

&lt;p&gt;I’d love to hear where this breaks down for other people. I truly find this code easier to follow and maintain, but at the price of deviating from a community standard. Ultimately, we always need to decide what’s more important.&lt;/p&gt;

&lt;p&gt;And by the way, I’m sure that there is a performance penalty of using anonymous functions over private functions. I haven’t measured it (yet). But for non-critical paths this should not matter that much.&lt;/p&gt;</content><author><name></name></author><category term="elixir" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://katafrakt.me/assets/og-gen/ocaml-flavoured-elixir.png" /><media:content medium="image" url="https://katafrakt.me/assets/og-gen/ocaml-flavoured-elixir.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Micro-slices in Hanami</title><link href="https://katafrakt.me/2025/07/29/micro-slices-hanami/" rel="alternate" type="text/html" title="Micro-slices in Hanami" /><published>2025-07-29T00:01:00+02:00</published><updated>2025-07-29T00:01:00+02:00</updated><id>repo://posts.collection/_posts/2025-07-29-micro-slices-hanami.md</id><content type="html" xml:base="https://katafrakt.me/2025/07/29/micro-slices-hanami/">&lt;p&gt;Slices are my favourite architectural feature in Hanami. They allude to the &lt;a href=&quot;/notes/vertical_slices&quot;&gt;vertical slices&lt;/a&gt; architecture and let you host cross-functional “mini-apps” within your main Hanami project&lt;label for=&quot;6a986c79-44c7-4bda-89ca-a8452e52a856&quot; class=&quot;sidenote-toggle sidenote-number&quot;&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;6a986c79-44c7-4bda-89ca-a8452e52a856&quot; class=&quot;sidenote-toggle&quot; /&gt;&lt;span class=&quot;sidenote&quot;&gt;The feature itself is present from early days of Hanami, perhaps from the times when it was still called Lotus. Back then it was called &lt;code class=&quot;highlighter-rouge&quot;&gt;apps&lt;/code&gt;.&lt;/span&gt;.&lt;/p&gt;

&lt;p&gt;A typical slice file structure (generated by Hanami slice generator) would look like this:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;slices
└── admin
    ├── action.rb
    ├── actions
    ├── assets
    │   ├── css
    │   │   └── app.css
    │   ├── images
    │   │   └── favicon.ico
    │   └── js
    │       └── app.js
    ├── db
    │   ├── relation.rb
    │   ├── repo.rb
    │   └── struct.rb
    ├── operation.rb
    ├── relations
    ├── repos
    ├── structs
    ├── templates
    │   └── layouts
    │       └── app.html.erb
    ├── view.rb
    └── views
        └── helpers.rb

14 directories, 11 files
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is a great foundation to build a comprehensive, large slice, such as an admin panel. However, it feels a bit overwhelming for smaller-scope things.&lt;/p&gt;

&lt;p&gt;Yesterday, while spelunking through Hanami internals, looking for a way to distribute a slice as a gem&lt;label for=&quot;57109bc1-26ce-4ccd-9670-ed45a2f5b27f&quot; class=&quot;sidenote-toggle sidenote-number&quot;&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;57109bc1-26ce-4ccd-9670-ed45a2f5b27f&quot; class=&quot;sidenote-toggle&quot; /&gt;&lt;span class=&quot;sidenote&quot;&gt;Unfortunately, I think it’s not easily done right now.&lt;/span&gt;, I stumbled upon another way to define a slice. It is not described in the Guides, but is documented in the code - so I treat it as official.&lt;/p&gt;

&lt;p&gt;The trick is this:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Drop a file into &lt;code class=&quot;highlighter-rouge&quot;&gt;config/slices&lt;/code&gt; (a directory that doesn’t exist in a stock Hanami app)&lt;/li&gt;
  &lt;li&gt;That’s it. No subdirectory army required.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s build one.&lt;/p&gt;

&lt;h2 id=&quot;the-tutorial-part&quot;&gt;The tutorial part&lt;/h2&gt;

&lt;p&gt;We will start by creating a new Hanami app and creating a dir and a file.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;hanami new myapp
cd myapp
mkdir config/slices
touch config/slices/healthcheck.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now we will put an ultra-simple slice inside:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Healthcheck&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Slice&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Hanami&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Slice&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  
  &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Routes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Hanami&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Routes&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;to: &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
      &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; 
      &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;{&quot;status&quot;: &quot;ok&quot;}&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Finally, we mount this in our main app, in &lt;code class=&quot;highlighter-rouge&quot;&gt;config/routes.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Myapp&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Routes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Hanami&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Routes&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;slice&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:healthcheck&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;at: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/health&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now if we start the app, we can check that it works:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ curl -i localhost:2300/health
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 16

{&quot;status&quot;: &quot;ok&quot;}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Our micro-slice is up and running.&lt;/p&gt;

&lt;h2 id=&quot;why-reach-for-micro-slices&quot;&gt;Why reach for micro-slices&lt;/h2&gt;

&lt;p&gt;As shown above, this is a great way to create a very focused, small slice, without all the ceremony of creating a directory in the &lt;code class=&quot;highlighter-rouge&quot;&gt;slices&lt;/code&gt; directory. It might be useful if you mainly organize the slices by domain boundaries, but still need a couple of “technical” (non-domain) ones, for example to expose some metrics or to have an admin dashboard.&lt;/p&gt;

&lt;p&gt;In absence of a good way to have externally packaged slices supported in Hanami&lt;label for=&quot;593fcc35-4bc6-4cbf-b428-a24d9873c51e&quot; class=&quot;sidenote-toggle sidenote-number&quot;&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;593fcc35-4bc6-4cbf-b428-a24d9873c51e&quot; class=&quot;sidenote-toggle&quot; /&gt;&lt;span class=&quot;sidenote&quot;&gt;… yet. I would like, at some point, to find time to try to solve this.&lt;/span&gt;, it might pave the way to ship a “slice builder” in a gem.&lt;/p&gt;

&lt;p&gt;Imagine shipping an “admin slice DSL” from a gem.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# config/slices/admin.rb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Admin&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;extend&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;GemifiedAdminPanelSliceBuilder&lt;/span&gt;
  
  &lt;span class=&quot;n&quot;&gt;admin&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;section&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;E-Commerce&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Orders&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;schema: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ECommerce&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Schemas&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Order&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Products&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;schema: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ECommerce&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Schemas&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Product&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Shipping methods&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;schema: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ECommerce&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Schemas&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ShippingMethod&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The gem isn’t the admin slice — it just gives you a way to build one. This is still day-dreaming, but micro-slices show a legit path towards this goal.&lt;/p&gt;

&lt;p&gt;Meanwhile, we can all enjoy our micro-slices, if needed.&lt;/p&gt;</content><author><name></name></author><category term="ruby" /><category term="hanami" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://katafrakt.me/assets/og-gen/micro-slices-hanami.png" /><media:content medium="image" url="https://katafrakt.me/assets/og-gen/micro-slices-hanami.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Ecto, on_replace and deferred checks</title><link href="https://katafrakt.me/2025/07/03/ecto-on-replace-deferred-check/" rel="alternate" type="text/html" title="Ecto, on_replace and deferred checks" /><published>2025-07-03T00:01:00+02:00</published><updated>2025-07-03T00:01:00+02:00</updated><id>repo://posts.collection/_posts/2025-07-03-ecto-on-replace-deferred-check.md</id><content type="html" xml:base="https://katafrakt.me/2025/07/03/ecto-on-replace-deferred-check/">&lt;p&gt;Today I learned a valuable lesson about how a seemingly simple task can have very rough edge cases, which take hours to solve. It involved Ecto, its associations and &lt;code class=&quot;highlighter-rouge&quot;&gt;on_replace&lt;/code&gt; option, and uniqueness checks in the database. Here’s the story.&lt;/p&gt;

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

&lt;p&gt;Let’s say you are modelling some kind of processes. These processes have steps and the steps have to be executed in a precise order. This is how a database structure for it would look like:&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;change&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:processes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:steps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:process_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;references&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:processes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:integer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It is fairly straightforward: the order of steps inside of a process is controlled by an &lt;code class=&quot;highlighter-rouge&quot;&gt;order&lt;/code&gt; integer column. Since it is important to always have the order of steps precise, we would like to additionally ensure it by a uniqie index:&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unique_index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:steps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:process_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We also know that the steps are only edited via a parent process. There is a form in the application, where you can edit the process fields and add, change, delete steps for it. The payload sent to a server always includes all the steps. Armed with that knowledge we create Ecto schemas like this:&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Core&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Process&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Ecto&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Schema&lt;/span&gt;
  &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Ecto&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Changeset&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;schema&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;processes&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;field&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:string&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;has_many&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:steps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Core&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Step&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;on_replace:&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;preload_order:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;asc:&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;process&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cast_assoc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:steps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Core&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Step&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Ecto&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Schema&lt;/span&gt;
  &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Ecto&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Changeset&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;schema&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;steps&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;field&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:string&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;field&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:integer&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;belongs_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Core&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Process&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;step&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I did that, quite happily, tested a bit with adding, changing and removing some steps. Finally I merged the pull request.&lt;/p&gt;

&lt;p&gt;It was only a few hours later when a colleague slacked me that something goes wrong. What he did was reordering the steps, or rather trying to do it. From this structure:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;step 1, order: 1
step 2, order: 2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;he wanted to go to&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;step 1, order: 2
step 2, order: 1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I quickly drafted code that replicated the issue:&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;proc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Process&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(%{&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;steps:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[%{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;order:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;First&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;%{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;order:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Second&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;insert!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;proc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;preload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;proc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:steps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;proc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;steps&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;s1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;%{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;id:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;order:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;s2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;%{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;id:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;order:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;proc&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Process&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(%{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;steps:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]})&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Attempt to run it resulted in an error:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;** (Ecto.ConstraintError) constraint error when attempting to update struct:

    * &quot;steps_process_id_order_index&quot; (unique_constraint)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It took me a while to realize what’s going on: Ecto tried to update the existing steps, one by one. So the first operation was:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Update “order” of step 1 to 2&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This left us with the following situation:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;step 1, order: 2
step 2, order: 2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This, quite obviously in retrospect, triggered the unique index. You cannot have 2 steps for one process with the same order! It did not matter that right after you were going to update step 2’s order to 1. The index operates here and now, you cannot do that.&lt;/p&gt;

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

&lt;p&gt;After discovering I was pretty close to writing my own hacky solution to update the steps, including inefficient approach with first deleting all and then creating all anew. Fortunately, in time, I vaguely remembered that there is something called &lt;a href=&quot;https://www.postgresql.org/docs/current/sql-set-constraints.html&quot;&gt;deferrable constraints&lt;/a&gt; in PostgreSQL.&lt;/p&gt;

&lt;p&gt;Deferrable constraint waits until the end of the transaction with checking if its condition is met. This was exactly what I was looking for! But an index cannot be deferrable in PostgreSQL. Luckily, another very similar construct - a uniqueness check - can.&lt;/p&gt;

&lt;p&gt;In the migration above, I had to replace the index creation with this:&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;execute&lt;/span&gt; &lt;span class=&quot;sd&quot;&gt;&quot;&quot;&quot;
ALTER TABLE steps ADD CONSTRAINT unique_step UNIQUE (process_id, &quot;order&quot;) DEFERRABLE INITIALLY DEFERRED
&quot;&quot;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With this statement we create a constraint on the &lt;code class=&quot;highlighter-rouge&quot;&gt;steps&lt;/code&gt; table which checks uniqueness of two columns and not only is deferrable - it is deferred to be checked on transaction commit by default.&lt;/p&gt;

&lt;p&gt;This is really all I had to do. The Ecto’s association replacing mechanism started working perfectly.&lt;/p&gt;

&lt;p&gt;This proved once again a truth I knew: the database is not a dumb storage you can just gloss over. It always pays off to learn it, its capabilities and intricacies. Because then you end up with a simple change instead of rewriting part of Ecto, but poorly.&lt;/p&gt;</content><author><name></name></author><category term="elixir" /><category term="ecto" /><category term="database" /><category term="postgresql" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://katafrakt.me/assets/og-gen/ecto-on-replace-deferred-check.png" /><media:content medium="image" url="https://katafrakt.me/assets/og-gen/ecto-on-replace-deferred-check.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Putting Hanami in the browser via WASM</title><link href="https://katafrakt.me/2025/06/23/hanami-on-wasm/" rel="alternate" type="text/html" title="Putting Hanami in the browser via WASM" /><published>2025-06-23T00:01:00+02:00</published><updated>2025-06-23T00:01:00+02:00</updated><id>repo://posts.collection/_posts/2025-06-23-hanami-on-wasm.md</id><content type="html" xml:base="https://katafrakt.me/2025/06/23/hanami-on-wasm/">&lt;p&gt;The other day I was loosely listening to a &lt;a href=&quot;https://podcast.drbragg.dev/&quot;&gt;Code and the Coding Coders who Code it&lt;/a&gt; podcast episode with Vladimir Dementyev. He was talking about his work around putting Ruby on Rails into the browser to lower the entry barrier for folks who just want to “feel” it, but witohut going through the struggle of choosing a correct Ruby version manager, installing dependencies for gems with C extensions etc.&lt;/p&gt;

&lt;p&gt;And I thought: Wow, great!&lt;/p&gt;

&lt;p&gt;And I also thought: If it’s (almost) possible with Rails, it should be even more possible with a truly modular framework, such as Hanami.&lt;/p&gt;

&lt;p&gt;Despite not knowing a thing about WASM, I decided to give it a go. I had my first working version running in about half an hour. Then the reality struck and I spent another 6 hours figuring stuff, reading code by Vladimir and his e-book and banging my head against my keyboard.&lt;/p&gt;

&lt;p&gt;Fortunately, after all this, I had a working version of a Hanami action (with validations) in the browser. You can get the code &lt;a href=&quot;https://codeberg.org/katafrakt/hanami-wasm&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since the WASM package is 50MB in size, it does not fit Cloudflare Pages limits and I have read too many stories about putting large files on Netlify. So I bought a very cheap VPS and put the demo there. &lt;a href=&quot;https://test.katafrakt.me/hanami-wasm/&quot;&gt;Here&lt;/a&gt; it is (I hope it still works when you are reading this post).&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/hanami-wasm.png&quot; alt=&quot;Screenshot of the website above, running a Hanami action code in an editor and providing a form to simulate parameters sending&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;some-details-and-thoughts&quot;&gt;Some details and thoughts&lt;/h2&gt;

&lt;p&gt;This is a bit unstructured feed of my thoughts and learnings from this experiment:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Everything related to Ruby and WASM goes through the  &lt;a href=&quot;https://github.com/ruby/ruby.wasm&quot;&gt;ruby.wasm&lt;/a&gt; repository, which is under Ruby official org on Github. It mainly (for me) contains a &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby_wasm&lt;/code&gt; tool, available via gem, which does all the heavy lifting.&lt;/li&gt;
  &lt;li&gt;However, things are not that simple when you need to have custom gems available in your WASM package. In my case the problem was with &lt;code class=&quot;highlighter-rouge&quot;&gt;Hanami::Utils&lt;/code&gt;, which &lt;a href=&quot;https://github.com/hanami/utils/blob/eea867f9f664c89a8e3523d3b1278b20425045a4/lib/hanami/utils/kernel.rb#L7-L8&quot;&gt;rely on BigDecimal&lt;/a&gt; at the time of writing this. &lt;code class=&quot;highlighter-rouge&quot;&gt;BigDecimal&lt;/code&gt; is one of the gems known to not compile to WASM due to… something.&lt;/li&gt;
  &lt;li&gt;To resolve the above, I had to resort to a trick mentioned by Vladimir in the podcast - provide a &lt;a href=&quot;https://codeberg.org/katafrakt/hanami-wasm/src/branch/main/shims/bigdecimal.rb&quot;&gt;dummy implementation&lt;/a&gt; of BigDecimal instead.&lt;/li&gt;
  &lt;li&gt;… and I also had to force WASM compiler to skip attempting to compile the gem by &lt;a href=&quot;https://codeberg.org/katafrakt/hanami-wasm/src/branch/main/build-wasm.rb#L6&quot;&gt;overriding its internals&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;I learned a lot from Vladimir’s e-book &lt;a href=&quot;https://writebook-on-wasm.fly.dev/5/ruby-on-rails-on-webassembly&quot;&gt;&lt;em&gt;Ruby on Rails on WebAssembly&lt;/em&gt;&lt;/a&gt; and probably even more from &lt;a href=&quot;https://github.com/palkan/wasmify-rails/&quot;&gt;the code he wrote&lt;/a&gt;. These are excellent resources if you want to learn about Ruby and WASM. As my example proves, you can get started in quite short time.&lt;/li&gt;
  &lt;li&gt;Remember that this is just an action with validation (&lt;code class=&quot;highlighter-rouge&quot;&gt;hanami-controller&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;hanami-validations&lt;/code&gt; gems), instrumented manually, not going through any router and not doing anything like and actual HTTP request. So this is quite far from putting the whole framework in WASM - also because of current limitations of WASM and its Ruby support.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;some-code-snippets&quot;&gt;Some code snippets&lt;/h2&gt;

&lt;p&gt;This is perhaps the most important part - fetching the WASM package and initializing a virtual machine with our gems and shims:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wasmResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;wasmUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wasmBuffer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wasmResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;arrayBuffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;WASM buffer size:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wasmBuffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;byteLength&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;WebAssembly&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;compile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;wasmBuffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;vm&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DefaultRubyVM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Custom WASM loaded successfully!&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;vm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`
    $LOAD_PATH.unshift &quot;/shims&quot;
    require &quot;/bundle/setup&quot;
    require &quot;hanami/controller&quot;

    module WasmApp
      class Action &amp;lt; Hanami::Action
      end
    end

    def run_action(cls, request)
      action = cls.new
      response = action.call(request)
      response.body.first
    end
`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;vm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And this is the code taking contents from the editor and running it, using the &lt;code class=&quot;highlighter-rouge&quot;&gt;run_action&lt;/code&gt; method defined above:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;runRubyCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;isRunning&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;vm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;vm&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;initVM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ruby&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;editor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;editor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;editorValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;serializedParams&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;serializeParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;paramsString&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;serializedParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;vm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ruby&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;
            run_action(TestAction, &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;paramsString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;)
        `&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;isRunning&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Execution failed:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;isRunning&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This also provides very basic error handling and will put the error in the browser.&lt;/p&gt;

&lt;p&gt;Finally, the builder script that assembles a WASM package to be used by frontend:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ruby_wasm&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ruby_wasm/packager&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ruby_wasm/cli&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;fileutils&quot;&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;RubyWasm&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Packager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EXCLUDED_GEMS&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;bigdecimal&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cli&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;RubyWasm&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;CLI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;stdout: &lt;/span&gt;&lt;span class=&quot;vg&quot;&gt;$stdout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;stderr: &lt;/span&gt;&lt;span class=&quot;vg&quot;&gt;$stderr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sx&quot;&gt;%W(build --ruby-version 3.4 -o ruby-web-temp.wasm --build-profile full --stdlib)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cli&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sx&quot;&gt;%w(pack ruby-web-temp.wasm --dir shims::/shims -o src/ruby-web.wasm)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cli&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;FileUtils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ruby-web-temp.wasm&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is all I have. It was a fun experiment with a completely new (to me) technology. Apparently it’s not hard to start with, but there’s a lot of dragons and rough edges still. But I learned something new and it was worth it.&lt;/p&gt;</content><author><name></name></author><category term="ruby" /><category term="hanami" /><category term="wasm" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://katafrakt.me/assets/og-gen/hanami-on-wasm.png" /><media:content medium="image" url="https://katafrakt.me/assets/og-gen/hanami-on-wasm.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>