https://blog.arkency.com/Hi, we're Arkency2024-03-26T15:11:04ZArkencyhttps://arkency.comtag:blog.arkency.com,2024-03-26:/do-you-tune-out-ruby-deprecation-warnings/Do you tune out Ruby deprecation warnings?2024-03-26T15:11:04Z2024-03-26T15:11:04Z<h1 id="do_you_tune_out_ruby_deprecation_warnings_">Do you tune out Ruby deprecation warnings?</h1>
<p>Looking into deprecation warnings is an essential habit to maintain an up-to-date tech stack.
Thanks to the explicit configuration of <code>ActiveSupport::Deprecation</code> in the environment-specific configuration
files, it’s quite common to handle deprecation warnings coming from Rails.
However, I rarely see projects configured properly to handle deprecation warnings coming from Ruby itself.
As we always want to keep both Rails and Ruby up-to-date, it’s crucial to handle both types of deprecation warnings.</p>
<h2 id="how_does_rails_handle_its_deprecation_warnings_">How does Rails handle its deprecation warnings?</h2>
<p>In the environment configuration files, Rails sets up the <code>ActiveSupport::Deprecation</code> like this:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># config/environments/development.rb</span>
<span class="c1"># Print deprecation notices to the Rails logger.</span>
<span class="n">config</span><span class="p">.</span><span class="nf">active_support</span><span class="p">.</span><span class="nf">deprecation</span> <span class="o">=</span> <span class="ss">:log</span>
<span class="c1"># Raise exceptions for disallowed deprecations.</span>
<span class="n">config</span><span class="p">.</span><span class="nf">active_support</span><span class="p">.</span><span class="nf">disallowed_deprecation</span> <span class="o">=</span> <span class="ss">:raise</span>
<span class="c1"># Tell Active Support which deprecation messages to disallow.</span>
<span class="n">config</span><span class="p">.</span><span class="nf">active_support</span><span class="p">.</span><span class="nf">disallowed_deprecation_warnings</span> <span class="o">=</span> <span class="p">[]</span>
</code></pre></div>
<p>It simply means that, in the development environment, all deprecation warnings will be logged to the Rails logger and if
there are any deprecations we won’t accept, an exception will be raised.
We usually want to disallow for deprecations that we have already handled to avoid regressions.</p>
<p>Available behaviors for <code>config.active_support.deprecation</code> are: <code>:raise</code>, <code>:stderr</code>, <code>:log</code>, <code>:notify</code>, <code>:report</code>, and
<code>:silence</code>. You can also pass any object that responds to the <code>call</code> method, i.e. a lambda.</p>
<p>We usually set it to <code>:raise</code> or <code>:log</code> in the development. It’s a good practice to collect them into an artifact on CI
in the test environment.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># config/environments/test.rb</span>
<span class="k">if</span> <span class="no">ENV</span><span class="p">.</span><span class="nf">has_key?</span><span class="p">(</span><span class="s1">'CI'</span><span class="p">)</span>
<span class="n">logger</span> <span class="o">=</span> <span class="no">Logger</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'log/deprecations.txt'</span><span class="p">)</span>
<span class="n">config</span><span class="p">.</span><span class="nf">active_support</span><span class="p">.</span><span class="nf">deprecation</span> <span class="o">=</span> <span class="n">logger</span><span class="p">.</span><span class="nf">method</span><span class="p">(</span><span class="ss">:info</span><span class="p">)</span>
<span class="k">else</span>
<span class="n">config</span><span class="p">.</span><span class="nf">active_support</span><span class="p">.</span><span class="nf">deprecation</span> <span class="o">=</span> <span class="ss">:log</span>
<span class="k">end</span>
</code></pre></div>
<p>In the production environment, on the other hand, we normally want to log but never raise.
However, an auto-generated <code>config/environments/production.rb</code> file sets
<code>config.active_support.report_deprecations = false</code> which is equivalent to <code>:silence</code> behaviour.
We need manual intervention to start collecting deprecation warnings from the production environment.</p>
<h2 id="how_about_ruby_deprecation_warnings_">How about Ruby deprecation warnings?</h2>
<p>Ruby can also emit deprecation warnings, but it’s not as straightforward as in Rails and requires an explicit setup.</p>
<p>It uses the built-in <code>Warning</code> module to notify about deprecated features being used.
However, by default warnings issued by Ruby are printed to <code>$stderr</code>, which is usually ignored by developers.
Moreover, <a href="https://bugs.ruby-lang.org/issues/17591">Ruby starting from version 2.7.2</a>, would not issue this certain type
of warning unless we explicitly tell it to do so with <code>Warning[:deprecated] = true</code>.</p>
<p>An approach that I recommend is to apply the same strategy to Ruby deprecation warnings as it is configured for Rails.</p>
<p>We can do it by overriding the <code>Kernel#warn</code> method, which is used by Ruby to print warnings and make it pass certain
messages to the <code>ActiveSupport::Deprecation#warn</code> method.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># config/initializers/capture_ruby_warnings.rb</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">deprecators</span><span class="p">[</span><span class="ss">:ruby</span><span class="p">]</span> <span class="o">=</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Deprecation</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="kp">nil</span><span class="p">,</span> <span class="s1">'Ruby'</span><span class="p">)</span>
<span class="k">module</span> <span class="nn">CaptureRubyWarnings</span>
<span class="k">def</span> <span class="nf">warn</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="ss">category: </span><span class="kp">nil</span><span class="p">)</span>
<span class="k">if</span> <span class="n">category</span> <span class="o">==</span> <span class="ss">:deprecated</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">deprecators</span><span class="p">[</span><span class="ss">:ruby</span><span class="p">].</span><span class="nf">warn</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">message</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="nb">caller</span><span class="p">)</span>
<span class="k">else</span>
<span class="k">super</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">Warning</span><span class="p">[</span><span class="ss">:deprecated</span><span class="p">]</span> <span class="o">=</span> <span class="kp">true</span>
<span class="no">Warning</span><span class="p">.</span><span class="nf">extend</span><span class="p">(</span><span class="no">CaptureRubyWarnings</span><span class="p">)</span>
</code></pre></div>
<figcaption align="center">
Ruby >= 3, Rails >= 7.1
</figcaption>
<p><a href="https://bugs.ruby-lang.org/issues/17122">Before Ruby 3</a>, there was not a <code>category</code> keyword argument in
the <code>Kernel#warn</code> method, so we have to perform some string matching to determine if the message is a deprecation
warning if we are on Ruby < 3.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># config/initializers/capture_ruby_warnings.rb</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">deprecators</span><span class="p">[</span><span class="ss">:ruby</span><span class="p">]</span> <span class="o">=</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Deprecation</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="kp">nil</span><span class="p">,</span> <span class="s1">'Ruby'</span><span class="p">)</span>
<span class="k">module</span> <span class="nn">CaptureRubyWarnings</span>
<span class="k">def</span> <span class="nf">warn</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
<span class="k">if</span> <span class="n">message</span> <span class="o">=~</span> <span class="sr">/deprecated/i</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">deprecators</span><span class="p">[</span><span class="ss">:ruby</span><span class="p">].</span><span class="nf">warn</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">message</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="nb">caller</span><span class="p">)</span>
<span class="k">else</span>
<span class="k">super</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">Warning</span><span class="p">[</span><span class="ss">:deprecated</span><span class="p">]</span> <span class="o">=</span> <span class="kp">true</span> <span class="k">if</span> <span class="no">Warning</span><span class="p">.</span><span class="nf">respond_to?</span><span class="p">(</span><span class="ss">:[]=</span><span class="p">)</span> <span class="c1"># Warning responds to []= since Ruby 2.7.0</span>
<span class="no">Warning</span><span class="p">.</span><span class="nf">extend</span><span class="p">(</span><span class="no">CaptureRubyWarnings</span><span class="p">)</span>
</code></pre></div>
<figcaption align="center">
Ruby < 3, Rails >= 7.1
</figcaption>
<p><a href="https://github.com/rails/rails/pull/46049">Prior to Rails 7.1</a>, there were not a collection of deprecators in
application configuration to define one per dependency. We used to call global <code>ActiveSupport::Deprecation</code> singleton
directly back then.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># config/initializers/capture_ruby_warnings.rb</span>
<span class="k">module</span> <span class="nn">CaptureRubyWarnings</span>
<span class="k">def</span> <span class="nf">warn</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="ss">category: </span><span class="kp">nil</span><span class="p">)</span>
<span class="k">if</span> <span class="n">category</span> <span class="o">==</span> <span class="ss">:deprecated</span>
<span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Deprecation</span><span class="p">.</span><span class="nf">warn</span><span class="p">(</span><span class="s2">"[RUBY] </span><span class="si">#{</span><span class="n">message</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="nb">caller</span><span class="p">)</span>
<span class="k">else</span>
<span class="k">super</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">Warning</span><span class="p">[</span><span class="ss">:deprecated</span><span class="p">]</span> <span class="o">=</span> <span class="kp">true</span>
<span class="no">Warning</span><span class="p">.</span><span class="nf">extend</span><span class="p">(</span><span class="no">CaptureRubyWarnings</span><span class="p">)</span>
</code></pre></div>
<figcaption align="center">
Ruby >= 3, Rails < 7.1
</figcaption>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># config/initializers/capture_ruby_warnings.rb</span>
<span class="k">module</span> <span class="nn">CaptureRubyWarnings</span>
<span class="k">def</span> <span class="nf">warn</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
<span class="k">if</span> <span class="n">message</span> <span class="o">=~</span> <span class="sr">/deprecated/i</span>
<span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Deprecation</span><span class="p">.</span><span class="nf">warn</span><span class="p">(</span><span class="s2">"[RUBY] </span><span class="si">#{</span><span class="n">message</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="nb">caller</span><span class="p">)</span>
<span class="k">else</span>
<span class="k">super</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">Warning</span><span class="p">[</span><span class="ss">:deprecated</span><span class="p">]</span> <span class="o">=</span> <span class="kp">true</span> <span class="k">if</span> <span class="no">Warning</span><span class="p">.</span><span class="nf">respond_to?</span><span class="p">(</span><span class="ss">:[]=</span><span class="p">)</span> <span class="c1"># Warning responds to []= since Ruby 2.7.0</span>
<span class="no">Warning</span><span class="p">.</span><span class="nf">extend</span><span class="p">(</span><span class="no">CaptureRubyWarnings</span><span class="p">)</span>
</code></pre></div>
<figcaption align="center">
Ruby < 3, Rails < 7.1
</figcaption>
tag:blog.arkency.com,2024-03-15:/how-to-get-burned-by-16-years-old-hack-in-2024/How to get burned by 16 years old hack in 20242024-03-15T10:37:05Z2024-03-15T10:37:05Z<p>There’s a project I’m consulting on where programmers develop predominantly in cloud environment. This setup simplifies a lot of moving parts and has the benefit of providing everyone homogenous containers to run code. If it runs on my box — it will run on everyone’s box. In that case, that box is Linux-based. It has the drawback of having greater latency and being more resource-constrained than a beefy local machine a developer is equipped with, i.e. MacBook Pro running on Apple Silicon.</p>
<p>Recently we’ve upgraded this development environment from Ruby 3.2.2 to Ruby 3.3.0. The process was smooth and predictable in the cloud environment. It worked on my box and by definition on everyone’s boxes. However this wasn’t always the case for local machines. The developers who chose to upgrade Ruby on their Macs early, experienced no trouble either. On the other hand, those who procrastinated a bit…</p>
<p>The developers who procrastinated with Ruby upgrade got caught by the new release of Apple’s Command Line Tools. If you’re curious that version was:</p>
<div class="highlight"><pre class="highlight plaintext"><code>$ pkgutil --pkg-info=com.apple.pkg.CLTools_Executables
package-id: com.apple.pkg.CLTools_Executables
version: 15.3.0.0.1.1708646388
volume: /
location: /
install-time: 1710339117
</code></pre></div>
<p>Why would a new release of system tooling introduce a burden to run Ruby application on a newer version of Ruby VM? It’s the gems! Specifically the gems with C-extensions, that rely on the system tooling to compile its binaries from source.</p>
<p>Among the lines in <code>Gemfile</code> one could find these two responsible for remote debugging in cloud development boxes:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="n">group</span> <span class="ss">:development</span> <span class="k">do</span>
<span class="n">gem</span> <span class="s1">'ruby-debug-ide'</span><span class="p">,</span> <span class="s1">'0.7.3'</span>
<span class="n">gem</span> <span class="s1">'debase'</span><span class="p">,</span> <span class="s1">'0.2.5.beta2'</span>
<span class="k">end</span>
</code></pre></div>
<p>The culprit was in the debase gem, which did not build on new Command Line Tools. </p>
<div class="highlight"><pre class="highlight plaintext"><code>Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
current directory: /Users/kakadudu/.rvm/gems/ruby-3.3.0/bundler/gems/ruby-debug-ide-b671a1cbb6d8/ext
/Users/kakadudu/.rvm/rubies/ruby-3.3.0/bin/ruby mkrf_conf.rb
Installing base gem
Building native extensions. This could take a while...
Building native extensions. This could take a while...
ERROR: Failed to build gem native extension.
current directory: /Users/kakadudu/.rvm/gems/ruby-3.3.0/bundler/gems/debase-0.2.5.beta2/ext
/Users/kakadudu/.rvm/rubies/ruby-3.3.0/bin/ruby extconf.rb
checking for vm_core.h... yes
checking for iseq.h... yes
checking for version.h... yes
creating Makefile
current directory: /Users/kakadudu/.rvm/gems/ruby-3.3.0/bundler/gems/debase-0.2.5.beta2/ext
make DESTDIR\= sitearchdir\=./.gem.20240311-43199-d198st sitelibdir\=./.gem.20240311-43199-d198st clean
current directory: /Users/kakadudu/.rvm/gems/ruby-3.3.0/bundler/gems/debase-0.2.5.beta2/ext
make DESTDIR\= sitearchdir\=./.gem.20240311-43199-d198st sitelibdir\=./.gem.20240311-43199-d198st
compiling breakpoint.c
compiling context.c
compiling debase_internals.c
debase_internals.c:319:25: warning: initializing 'rb_control_frame_t *' (aka 'struct rb_control_frame_struct *') with an expression of type 'const
rb_control_frame_t *' (aka 'const struct rb_control_frame_struct *') discards qualifiers [-Wincompatible-pointer-types-discards-qualifiers]
rb_control_frame_t *start_cfp = RUBY_VM_END_CONTROL_FRAME(TH_INFO(thread));
^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
debase_internals.c:770:3: error: incompatible function pointer types passing 'void (VALUE, VALUE)' (aka 'void (unsigned long, unsigned long)') to parameter
of type 'VALUE (*)(VALUE, VALUE)' (aka 'unsigned long (*)(unsigned long, unsigned long)') [-Wincompatible-function-pointer-types]
rb_define_module_function(mDebase, "set_trace_flag_to_iseq", Debase_set_trace_flag_to_iseq, 1);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/Users/kakadudu/.rvm/rubies/ruby-3.3.0/include/ruby-3.3.0/ruby/internal/anyargs.h:338:142: note: expanded from macro 'rb_define_module_function'
#define rb_define_module_function(mod, mid, func, arity) RBIMPL_ANYARGS_DISPATCH_rb_define_module_function((arity), (func))((mod), (mid), (func),
(arity))
^~~~~~
/Users/kakadudu/.rvm/rubies/ruby-3.3.0/include/ruby-3.3.0/ruby/internal/anyargs.h:274:1: note: passing argument to parameter here
RBIMPL_ANYARGS_DECL(rb_define_module_function, VALUE, const char *)
^
/Users/kakadudu/.rvm/rubies/ruby-3.3.0/include/ruby-3.3.0/ruby/internal/anyargs.h:256:72: note: expanded from macro 'RBIMPL_ANYARGS_DECL'
RBIMPL_ANYARGS_ATTRSET(sym) static void sym ## _01(__VA_ARGS__, VALUE(*)(VALUE, VALUE), int); \
^
debase_internals.c:773:3: error: incompatible function pointer types passing 'void (VALUE, VALUE)' (aka 'void (unsigned long, unsigned long)') to parameter
of type 'VALUE (*)(VALUE, VALUE)' (aka 'unsigned long (*)(unsigned long, unsigned long)') [-Wincompatible-function-pointer-types]
rb_define_module_function(mDebase, "unset_iseq_flags", Debase_unset_trace_flags, 1);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/Users/kakadudu/.rvm/rubies/ruby-3.3.0/include/ruby-3.3.0/ruby/internal/anyargs.h:338:142: note: expanded from macro 'rb_define_module_function'
#define rb_define_module_function(mod, mid, func, arity) RBIMPL_ANYARGS_DISPATCH_rb_define_module_function((arity), (func))((mod), (mid), (func),
(arity))
^~~~~~
/Users/kakadudu/.rvm/rubies/ruby-3.3.0/include/ruby-3.3.0/ruby/internal/anyargs.h:274:1: note: passing argument to parameter here
RBIMPL_ANYARGS_DECL(rb_define_module_function, VALUE, const char *)
^
/Users/kakadudu/.rvm/rubies/ruby-3.3.0/include/ruby-3.3.0/ruby/internal/anyargs.h:256:72: note: expanded from macro 'RBIMPL_ANYARGS_DECL'
RBIMPL_ANYARGS_ATTRSET(sym) static void sym ## _01(__VA_ARGS__, VALUE(*)(VALUE, VALUE), int); \
^
1 warning and 2 errors generated.
make: *** [debase_internals.o] Error 1
make failed, exit code 2
Gem files will remain installed in /Users/kakadudu/.rvm/gems/ruby-3.3.0/bundler/gems/debase-0.2.5.beta2 for inspection.
Results logged to /Users/kakadudu/.rvm/gems/ruby-3.3.0/bundler/extensions/x86_64-darwin-23/3.3.0/debase-0.2.5.beta2/gem_make.out
...
/Users/kakadudu/.rvm/rubies/ruby-3.3.0/lib/ruby/3.3.0/rubygems/dependency_installer.rb:250:in `install'
mkrf_conf.rb:31:in `rescue in <main>'
mkrf_conf.rb:24:in `<main>'
rake failed, exit code 1
Gem files will remain installed in /Users/kakadudu/.rvm/gems/ruby-3.3.0/bundler/gems/ruby-debug-ide-b671a1cbb6d8 for inspection.
Results logged to /Users/kakadudu/.rvm/gems/ruby-3.3.0/bundler/gems/extensions/x86_64-darwin-23/3.3.0/ruby-debug-ide-b671a1cbb6d8/gem_make.out
...
An error occurred while installing ruby-debug-ide (0.7.3), and Bundler cannot continue.
In Gemfile:
ruby-debug-ide
</code></pre></div>
<p>The build error message suggested the workaround to make it work again. If the gem was built with error condition turned off, it succeeded:</p>
<div class="highlight"><pre class="highlight plaintext"><code>gem install debase -v '0.2.5.beta2' -- --with-cflags=-Wno-error=incompatible-function-pointer-types
</code></pre></div>
<p>Translating this to bundler configuration, so that <code>bundle install</code> picks it up, seemed straightforward:</p>
<div class="highlight"><pre class="highlight plaintext"><code>bundle config build.debase --with-cflags=-Wno-error=incompatible-function-pointer-types
</code></pre></div>
<p>But it did not work. Why? </p>
<h2 id="the_16_years_old_hack">The 16 years old hack</h2>
<p>Looking again at the error message made me realise something. While the compiler could not build the <code>debase</code> gem, despite bundler having the right flags to instruct the compiler, it was the <code>ruby-debug-ide</code> gem which initiated the trouble.</p>
<p>This gem has no dependencies in its gemspec:</p>
<div class="highlight"><pre class="highlight plaintext"><code>$ gem dependency -r ruby-debug-ide -v '0.7.3'
Gem ruby-debug-ide-0.7.3
rake (>= 0.8.1)
</code></pre></div>
<p>Yet it initiates the build of the <code>debase</code> gem. A quick look into <code>ruby-debug-ide</code> source code revealed this gem ships a C-extension:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="no">Gem</span><span class="o">::</span><span class="no">Specification</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">spec</span><span class="o">|</span>
<span class="n">spec</span><span class="p">.</span><span class="nf">name</span> <span class="o">=</span> <span class="s2">"ruby-debug-ide"</span>
<span class="o">...</span>
<span class="n">spec</span><span class="p">.</span><span class="nf">extensions</span> <span class="o"><<</span> <span class="s2">"ext/mkrf_conf.rb"</span> <span class="k">unless</span> <span class="no">ENV</span><span class="p">[</span><span class="s1">'NO_EXT'</span><span class="p">]</span>
<span class="k">end</span>
</code></pre></div>
<p>And this C-extension is a fake one:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="n">install_dir</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">expand_path</span><span class="p">(</span><span class="s2">"../../../.."</span><span class="p">,</span> <span class="kp">__FILE__</span><span class="p">)</span>
<span class="k">if</span> <span class="o">!</span><span class="k">defined?</span><span class="p">(</span><span class="no">RUBY_ENGINE</span><span class="p">)</span> <span class="o">||</span> <span class="no">RUBY_ENGINE</span> <span class="o">==</span> <span class="s1">'ruby'</span>
<span class="nb">require</span> <span class="s1">'rubygems'</span>
<span class="nb">require</span> <span class="s1">'rubygems/command.rb'</span>
<span class="nb">require</span> <span class="s1">'rubygems/dependency.rb'</span>
<span class="nb">require</span> <span class="s1">'rubygems/dependency_installer.rb'</span>
<span class="k">begin</span>
<span class="no">Gem</span><span class="o">::</span><span class="no">Command</span><span class="p">.</span><span class="nf">build_args</span> <span class="o">=</span> <span class="no">ARGV</span>
<span class="k">rescue</span> <span class="no">NoMethodError</span>
<span class="k">end</span>
<span class="k">if</span> <span class="no">RUBY_VERSION</span> <span class="o"><</span> <span class="s2">"1.9"</span>
<span class="n">dep</span> <span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">Dependency</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"ruby-debug-base"</span><span class="p">,</span> <span class="s1">'>=0.10.4'</span><span class="p">)</span>
<span class="k">elsif</span> <span class="no">RUBY_VERSION</span> <span class="o"><</span> <span class="s1">'2.0'</span>
<span class="n">dep</span> <span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">Dependency</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"ruby-debug-base19x"</span><span class="p">,</span> <span class="s1">'>=0.11.30.pre15'</span><span class="p">)</span>
<span class="k">else</span>
<span class="n">dep</span> <span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">Dependency</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"debase"</span><span class="p">,</span> <span class="s1">'> 0'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">begin</span>
<span class="nb">puts</span> <span class="s2">"Installing base gem"</span>
<span class="n">inst</span> <span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">DependencyInstaller</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:prerelease</span> <span class="o">=></span> <span class="n">dep</span><span class="p">.</span><span class="nf">prerelease?</span><span class="p">,</span> <span class="ss">:install_dir</span> <span class="o">=></span> <span class="n">install_dir</span><span class="p">)</span>
<span class="n">inst</span><span class="p">.</span><span class="nf">install</span> <span class="n">dep</span>
<span class="k">rescue</span>
<span class="k">begin</span>
<span class="n">inst</span> <span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">DependencyInstaller</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:prerelease</span> <span class="o">=></span> <span class="kp">true</span><span class="p">,</span> <span class="ss">:install_dir</span> <span class="o">=></span> <span class="n">install_dir</span><span class="p">)</span>
<span class="n">inst</span><span class="p">.</span><span class="nf">install</span> <span class="n">dep</span>
<span class="k">rescue</span> <span class="no">Exception</span> <span class="o">=></span> <span class="n">e</span>
<span class="nb">puts</span> <span class="n">e</span>
<span class="nb">puts</span> <span class="n">e</span><span class="p">.</span><span class="nf">backtrace</span><span class="p">.</span><span class="nf">join</span> <span class="s2">"</span><span class="se">\n</span><span class="s2"> "</span>
<span class="nb">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span> <span class="k">unless</span> <span class="n">dep</span><span class="p">.</span><span class="nf">nil?</span> <span class="o">||</span> <span class="n">dep</span><span class="p">.</span><span class="nf">matching_specs</span><span class="p">.</span><span class="nf">any?</span>
<span class="k">end</span>
<span class="c1"># create dummy rakefile to indicate success</span>
<span class="n">f</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="no">File</span><span class="p">.</span><span class="nf">dirname</span><span class="p">(</span><span class="kp">__FILE__</span><span class="p">),</span> <span class="s2">"Rakefile"</span><span class="p">),</span> <span class="s2">"w"</span><span class="p">)</span>
<span class="n">f</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="s2">"task :default</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
<span class="n">f</span><span class="p">.</span><span class="nf">close</span>
</code></pre></div>
<p>I’ve accidentally learned about <code>Gem::DependencyInstaller</code>, which does not honor Bundler config and its build flags for gems. </p>
<p>The comment of <em>dummy rakefile to indicate success</em> made me explore this more and I’ve found <a href="https://github.com/search?q=path%3A*.gemspec+ext%2Fmkrf_conf.rb&type=code&ref=advsearch">180 similar results in gemspecs on github</a>.</p>
<p>Eventually I’ve found the pattern described on a wiki:
<a href="https://en.wikibooks.org/wiki/Ruby_Programming/RubyGems#How_to_install_different_versions_of_gems_depending_on_which_version_of_ruby_the_installee_is_using">How to install different versions of gems depending on which version of ruby the installee is using</a></p>
<p>Why does this pattern exist? Let’s zoom into to this conditional:</p>
<div class="highlight"><pre class="highlight ruby"><code> <span class="k">if</span> <span class="no">RUBY_VERSION</span> <span class="o"><</span> <span class="s2">"1.9"</span>
<span class="n">dep</span> <span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">Dependency</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"ruby-debug-base"</span><span class="p">,</span> <span class="s1">'>=0.10.4'</span><span class="p">)</span>
<span class="k">elsif</span> <span class="no">RUBY_VERSION</span> <span class="o"><</span> <span class="s1">'2.0'</span>
<span class="n">dep</span> <span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">Dependency</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"ruby-debug-base19x"</span><span class="p">,</span> <span class="s1">'>=0.11.30.pre15'</span><span class="p">)</span>
<span class="k">else</span>
<span class="n">dep</span> <span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">Dependency</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"debase"</span><span class="p">,</span> <span class="s1">'> 0'</span><span class="p">)</span>
<span class="k">end</span>
<span class="no">Gem</span><span class="o">::</span><span class="no">DependencyInstaller</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:prerelease</span> <span class="o">=></span> <span class="n">dep</span><span class="p">.</span><span class="nf">prerelease?</span><span class="p">,</span> <span class="ss">:install_dir</span> <span class="o">=></span> <span class="n">install_dir</span><span class="p">).</span><span class="nf">install</span><span class="p">(</span><span class="n">dep</span><span class="p">)</span>
</code></pre></div>
<p>This pattern is supposed to dynamically add dependencies, based on which Ruby VM version we’re installing this gem on. Perhaps by the time it was introduced it was the only possible solution.</p>
<p>Nowadays an application developer could take advantage of <a href="https://bundler.io/v2.5/man/gemfile.5.html#PLATFORMS">Bundler platforms</a> in <code>Gemfile</code>:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="n">gem</span> <span class="s2">"weakling"</span><span class="p">,</span> <span class="ss">platforms: :jruby</span>
<span class="n">gem</span> <span class="s2">"ruby-debug"</span><span class="p">,</span> <span class="ss">platforms: :mri_31</span>
<span class="n">gem</span> <span class="s2">"nokogiri"</span><span class="p">,</span> <span class="ss">platforms: </span><span class="p">[</span><span class="ss">:windows_31</span><span class="p">,</span> <span class="ss">:jruby</span><span class="p">]</span>
</code></pre></div>
<p>On the other hand a library developer distributing gems via rubygems.org can use a <a href="https://guides.rubygems.org/specification-reference/#platform=">platform specification</a>. This allows building different gems for different runtimes.
A good example of this is <code>google-protobuf</code> gem shipping 10 different packages (with pre-built binaries for each platform) for the same library release.</p>
<div class="highlight"><pre class="highlight plaintext"><code>4.26.0 - March 12, 2024 (255 KB)
4.26.0 - March 12, 2024 x86_64-darwin (916 KB)
4.26.0 - March 12, 2024 aarch64-linux (879 KB)
4.26.0 - March 12, 2024 x64-mingw-ucrt (698 KB)
4.26.0 - March 12, 2024 x64-mingw32 (532 KB)
4.26.0 - March 12, 2024 x86_64-linux (887 KB)
4.26.0 - March 12, 2024 x86-linux (915 KB)
4.26.0 - March 12, 2024 java (4.92 MB)
4.26.0 - March 12, 2024 arm64-darwin (876 KB)
4.26.0 - March 12, 2024 x86-mingw32 (901 KB)
</code></pre></div>
<p>Normally the gemspec Ruby-like specification is transformed into a more static one. The conditionals inside gemspec would not work. With <code>platform</code> we’re instructing to provide several specifications — each for the desired runtime.</p>
<div class="highlight"><pre class="highlight ruby"><code> <span class="k">if</span> <span class="no">RUBY_PLATFORM</span> <span class="o">==</span> <span class="s2">"java"</span>
<span class="n">s</span><span class="p">.</span><span class="nf">platform</span> <span class="o">=</span> <span class="s2">"java"</span>
<span class="n">s</span><span class="p">.</span><span class="nf">files</span> <span class="o">+=</span> <span class="p">[</span><span class="s2">"lib/google/protobuf_java.jar"</span><span class="p">]</span> <span class="o">+</span>
<span class="no">Dir</span><span class="p">.</span><span class="nf">glob</span><span class="p">(</span><span class="s1">'ext/**/*'</span><span class="p">).</span><span class="nf">reject</span> <span class="k">do</span> <span class="o">|</span><span class="n">file</span><span class="o">|</span>
<span class="no">File</span><span class="p">.</span><span class="nf">basename</span><span class="p">(</span><span class="n">file</span><span class="p">)</span> <span class="o">=~</span> <span class="sr">/^((convert|defs|map|repeated_field)\.[ch]|
BUILD\.bazel|extconf\.rb|wrap_memcpy\.c)$/x</span>
<span class="k">end</span>
<span class="n">s</span><span class="p">.</span><span class="nf">extensions</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"ext/google/protobuf_c/Rakefile"</span><span class="p">]</span>
<span class="n">s</span><span class="p">.</span><span class="nf">add_dependency</span> <span class="s2">"ffi"</span><span class="p">,</span> <span class="s2">"~>1"</span>
<span class="n">s</span><span class="p">.</span><span class="nf">add_dependency</span> <span class="s2">"ffi-compiler"</span><span class="p">,</span> <span class="s2">"~>1"</span>
<span class="k">else</span>
<span class="n">s</span><span class="p">.</span><span class="nf">files</span> <span class="o">+=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">glob</span><span class="p">(</span><span class="s1">'ext/**/*'</span><span class="p">).</span><span class="nf">reject</span> <span class="k">do</span> <span class="o">|</span><span class="n">file</span><span class="o">|</span>
<span class="no">File</span><span class="p">.</span><span class="nf">basename</span><span class="p">(</span><span class="n">file</span><span class="p">)</span> <span class="o">=~</span> <span class="sr">/^(BUILD\.bazel)$/</span>
<span class="k">end</span>
<span class="n">s</span><span class="p">.</span><span class="nf">extensions</span> <span class="o">=</span> <span class="sx">%w[
ext/google/protobuf_c/extconf.rb
ext/google/protobuf_c/Rakefile
]</span>
<span class="n">s</span><span class="p">.</span><span class="nf">add_development_dependency</span> <span class="s2">"rake-compiler-dock"</span><span class="p">,</span> <span class="s2">"= 1.2.1"</span>
<span class="k">end</span>
</code></pre></div>
<p>What did we do in our project? We’re not building Ruby IDEs for living. We do not need to support Ruby older than 3.3.0.</p>
<p>Thus we’ve:</p>
<ul>
<li>remained on stated explicit dependencies in <code>Gemfile</code></li>
<li>opted out from conditional dependencies — removed <code>spec.extensions=</code> line in <code>ruby-debug-ide</code> fork along with <code>ext/mkrf_conf.rb</code> file</li>
<li>enjoyed not seeing developers distracted by accidental complexity in the stack — without needing Bundler to support this pattern</li>
</ul>
tag:blog.arkency.com,2024-03-14:/how-to-add-a-loading-animation-to-your-turbo-frame-with-tailwindcss/How to add a loading animation to your turbo frame with TailwindCSS2024-03-14T12:06:22Z2024-03-14T12:06:22Z<h1 id="how_to_add_a_loading_animation_to_your_turbo_frame_with_tailwindcss">How to add a loading animation to your turbo frame with TailwindCSS</h1>
<p>Ever been working on a project and hit a snag? That’s what happened to me recently. I came across a turbo frame that was slow to load and didn’t show any signs of loading. Talk about confusing!</p>
<!-- more -->
<div class="not-prose">
<video src="https://arkency-images.s3.eu-central-1.amazonaws.com/how-to-add-a-loading-animation-to-your-turbo-frame-with-tailwindcss/loader-off.mp4" class="w-full" autoplay muted playsinline loop></video>
<span class="italic text-sm not-prose">Waiting a few eternities for the historic transactions tab to load.</span>
</div>
<h2 id="the__code_busy__code__attribute_of_the_turbo_frame">The <code>busy</code> attribute of the turbo frame</h2>
<p>The easiest way to add a loading state to the turbo frame is to insert the loader inside the frame tag. Problem is that it only works on the very first load, after that you’ll see the old content until the new one fully loads.</p>
<p>I did some digging and found out that <a href="https://turbo.hotwired.dev/reference/frames#html-attributes">turbo frames actually have states</a>, which can be useful: one when they’re loading <code>busy</code> and one when they’re done <code>complete</code>. They’re represented by an HTML attribute and can be used to create the proper CSS selector.</p>
<h2 id="the_handful_sibling_selector">The handful sibling selector</h2>
<p>To make my animation I’ve wrapped the frame with an additional container:</p>
<div class="highlight"><pre class="highlight erb"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"relative min-h-96"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">turbo_frame_tag</span> <span class="s1">'transactions'</span><span class="p">,</span> <span class="ss">src: </span><span class="n">dashboard_transactions_historic_path</span> <span class="k">do</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"></div></span>
</code></pre></div>
<p>I’ve added <code>relative</code> class to create a possibility of making overlay, and <code>min-h-96</code> - to make it at least 24rems height. As I’ve mentioned above,the <code>Loading...</code> part will show on the initial load. In this project we’re switching between different transaction types, and each of them has its own path, so after switching to another one (which takes a while to load) we’re left with the old view and no reaction from the UI. Let’s change it!</p>
<p>To create the overlay we need another element, which will change its behaviour based on turbo frame’s state. We’ll place it underneath the frame:</p>
<div class="highlight"><pre class="highlight erb"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"relative min-h-96"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">turbo_frame_tag</span> <span class="s1">'transactions'</span><span class="p">,</span> <span class="ss">src: </span><span class="n">dashboard_transactions_historic_path</span> <span class="k">do</span> <span class="cp">%></span>
Loading...
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-gray-50 bg-opacity-25 backdrop-blur-sm transition-opacity"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">image_tag</span> <span class="s2">"loading.svg"</span><span class="p">,</span> <span class="ss">class: </span><span class="s2">"animate-pulse"</span> <span class="cp">%></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
</code></pre></div>
<p>Right now we have a pulsating loading image with an overlay covering the frame’s content. We need to create a selector to change it’s opacity, to do it we’ll use the sibling <code>~</code> selector, and combine it with the tailwind’s arbitrary variant: <code>[[busy]~&]:</code>. In this puzzle <code>[busy]</code> refers to our frame, <code>&</code> represents the loader element, so when the frame get’s the <code>busy</code> attribute <code>[[busy]~&]:</code> variant will work. We’ll use it with the opacity property - default value will be <code>0</code>, and <code>100</code> for the active variant. We can also get rid of the <code>Loading...</code> text.</p>
<div class="highlight"><pre class="highlight erb"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"relative min-h-96"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">turbo_frame_tag</span> <span class="s1">'transactions'</span><span class="p">,</span> <span class="ss">src: </span><span class="n">dashboard_transactions_historic_path</span> <span class="k">do</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-gray-50 bg-opacity-25 opacity-0 [[busy]~&]:opacity-100 backdrop-blur-sm transition-opacity"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">image_tag</span> <span class="s2">"loading.svg"</span><span class="p">,</span> <span class="ss">class: </span><span class="s2">"animate-pulse"</span> <span class="cp">%></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
</code></pre></div>
<video src="https://arkency-images.s3.eu-central-1.amazonaws.com/how-to-add-a-loading-animation-to-your-turbo-frame-with-tailwindcss/loader-on.mp4" class="w-full" autoplay muted playsinline loop></video>
<p>Now everytime we reload the frame’s content we’ll get a visual confirmation that something is going on. Everything done with a plain CSS selector and not a single line of JavaScript!</p>
tag:blog.arkency.com,2024-02-12:/upcasting-events-in-railseventstore/Upcasting events in RailsEventStore2024-02-12T07:43:14Z2024-02-12T07:43:14Z<h1 id="upcasting_events_in_railseventstore">Upcasting events in RailsEventStore</h1>
<p>Understanding the domain you are working with often leads to the need to redesign some of the models. Sometimes you’ll
need to add or change a concept. Other times, you’ll need to remove a method or event produced by the aggregate. This was
the case for us.</p>
<p>Our goal was to remove an event from the system. To do this, we had to deal with the fact that this event was in the
aggregate stream.</p>
<p>It’s interesting how we got there.</p>
<p>We started discussing how to implement a new business feature in the aforementioned model.
After tossing around a few ideas it felt like it didn’t belong in the aggregate itself.
We realized that it belonged in the application layer, which is responsible for handling different business use cases.
It’s often a dilemma that can arise. Where does this business logic go? The aggregate, the application layer that is
responsible for the use cases? Somewhere else?
From a code perspective, it’s all about <em>where to put this if statement</em>.</p>
<p>Have you ever experienced a similar conundrum?</p>
<p>After some discussion, we decided to implement this feature in the application layer. But it felt hacky.
Writing a few test cases helped us realize that the aggregate class has two methods that basically do the same thing.
The concept is represented in the same way in the read model. However, those two methods produce different events.</p>
<p>Long story short, it turned out that our aggregate was a little too feature-driven. It worked fine, all the business
rules were respected. But it felt like it duplicated part of the business logic.</p>
<p>Feature-driven design of an aggregate deserves its own blog post. It’s not a bad place to start. Learning domain is a
process. With new insights, you may have to adjust the model.</p>
<p>At the end, of this somewhat long introduction, we realized that we had two events representing the same concept.
And we decided to remove one of them.</p>
<p>Then the question remains. What to do with the events that are already in the stream?</p>
<p><a href="https://blog.arkency.com/4-strategies-when-you-need-to-change-a-published-event/">There are multiple ways to deal with that situation.</a></p>
<p>We decided to upcast those events. Why? Events are immutable and shouldn’t be removed. Having those in stream will make it
easier to access them and understand the full picture whenever needed. It’s especially important when something goes
off.</p>
<h2 id="what_is_upcasting_">What is upcasting?</h2>
<p>Upcasting is the process of converting an event to a newer version of the event. In our case, the event was
duplicated.
As I mentioned earlier, we discovered this while implementing a new business use case. It turned out that two of the
events
represent the same concept.</p>
<p>In our case, upcasting will convert the old event that was duplicated to the other one that was originally there.
and should be the only one that represents that business concept.</p>
<h2 id="how_to_upcast_events_in_railseventstore_">How to upcast events in RailsEventStore?</h2>
<p>There are probably several ways to implement the details. However, the general idea is to use a
transformation
to the pipeline. In the transformation, we setup the RailsEventStore to convert the old event to the new one.</p>
<p>Take a look at the example below.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="no">RubyEventStore</span><span class="o">::</span><span class="no">Mappers</span><span class="o">::</span><span class="no">Pipeline</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
<span class="c1"># ... other transformations</span>
<span class="no">RubyEventStore</span><span class="o">::</span><span class="no">Mappers</span><span class="o">::</span><span class="no">Transformation</span><span class="o">::</span><span class="no">Upcast</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
<span class="p">{</span>
<span class="s2">"Module::RemovedEvent"</span> <span class="o">=></span> <span class="o">-></span><span class="p">(</span><span class="n">record</span><span class="p">)</span> <span class="k">do</span>
<span class="no">RubyEventStore</span><span class="o">::</span><span class="no">Record</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
<span class="ss">event_id: </span><span class="n">record</span><span class="p">.</span><span class="nf">event_id</span><span class="p">,</span>
<span class="ss">metadata: </span><span class="n">record</span><span class="p">.</span><span class="nf">metadata</span><span class="p">,</span>
<span class="ss">timestamp: </span><span class="n">record</span><span class="p">.</span><span class="nf">timestamp</span><span class="p">,</span>
<span class="ss">valid_at: </span><span class="n">record</span><span class="p">.</span><span class="nf">valid_at</span><span class="p">,</span>
<span class="ss">event_type: </span><span class="s2">"Module::TheOtherEvent"</span><span class="p">,</span>
<span class="ss">data: </span><span class="n">record</span><span class="p">.</span><span class="nf">data</span>
<span class="p">)</span>
<span class="k">end</span>
<span class="p">}</span>
<span class="p">)</span>
<span class="p">)</span>
</code></pre></div>
<p>The pipeline mapper, referred to by the keyword <code>mapper</code>, is a place where you can add various transformations.</p>
<p>The <code>Upcast</code> transformation takes hash as an argument. The key is the name of the old event. The value is an lambda,
that takes the old event record and returns a new one. Or in our case, existing one that was duplicated.</p>
<p>There are transformations available in the RailsEventStore. You can take a look at them for reference. For example,
the <a href="https://github.com/RailsEventStore/rails_event_store/blob/b8e4bbffabf43db98a154ebab694486229c3706c/ruby_event_store/lib/ruby_event_store/mappers/transformation/symbolize_metadata_keys.rb"><code>SymbolizeMetadataKeys</code></a>
or <a href="https://github.com/RailsEventStore/rails_event_store/blob/b8e4bbffabf43db98a154ebab694486229c3706c/contrib/ruby_event_store-transformations/lib/ruby_event_store/transformations/with_indifferent_access.rb"><code>WithIndifferentAccess</code></a>
transformations.
It’s also worth familiarizing yourself with
the <a href="https://github.com/RailsEventStore/rails_event_store/blob/b8e4bbffabf43db98a154ebab694486229c3706c/rails_event_store/lib/rails_event_store/json_client.rb"><code>JSONClient</code></a>.</p>
<h2 id="when_to_use_upcasting_">When to use upcasting?</h2>
<p>If you are using event sourcing, <strong>it is not recommended that you delete events</strong>. Events are <strong>immutable</strong> and
should be left as they were. This makes a lot of sense. It makes the application more reliable. It’s good for auditing.</p>
<p>Alternatively, if you know the event shouldn’t be in the stream, you could rewrite the stream and only include the
events that should be there. In this case, it felt unnecessary. The event was valid, it was just duplicated.</p>
<p>It’s good to learn different strategies and know when to use them. In this case, upcasting seemed to be the best
solution.</p>
tag:blog.arkency.com,2024-02-06:/completely-custom-zeitwerk-inflector/Completely custom Zeitwerk inflector2024-02-06T16:01:52Z2024-02-06T16:01:52Z<h1 id="completely_custom_zeitwerk_inflector">Completely custom Zeitwerk inflector</h1>
<p>In <a href="https://blog.arkency.com/the-mysterious-litany-of-require-depndency-calls/">my previous post</a>, I discussed the
difference between how the classic autoloader and Zeitwerk autoloader match constant and file names. Short reminder:</p>
<ul>
<li>Classic autoloader maps missing constant name <code>Report::PL::X123</code> to a file name by
calling <code>Report::PL::X123.to_s.underscore</code></li>
<li>Zeitwerk autoloader finds <code>lib/report/pl/x123/products.rb</code> and maps it to <code>Report::PL::X123::Products</code> constant name
with the help of defined <strong>inflectors</strong> rules.</li>
</ul>
<h2 id="what_is_an_inflector_">What is an inflector?</h2>
<p>In general, an inflector is a software component responsible for transforming words according to predefined rules.
In the context of web frameworks like Ruby on Rails, inflectors are used to handle different linguistic transformations,
such as pluralization, singularization, <strong>acronym handling</strong>, and humanization of attribute names.</p>
<p><code>Rails::Autoloader::Inflector</code> is the one that is used by default in Rails integration with Zeitwerk:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="k">module</span> <span class="nn">Rails</span>
<span class="k">class</span> <span class="nc">Autoloaders</span>
<span class="k">module</span> <span class="nn">Inflector</span> <span class="c1"># :nodoc:</span>
<span class="vi">@overrides</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">camelize</span><span class="p">(</span><span class="n">basename</span><span class="p">,</span> <span class="n">_abspath</span><span class="p">)</span>
<span class="vi">@overrides</span><span class="p">[</span><span class="n">basename</span><span class="p">]</span> <span class="o">||</span> <span class="n">basename</span><span class="p">.</span><span class="nf">camelize</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">inflect</span><span class="p">(</span><span class="n">overrides</span><span class="p">)</span>
<span class="vi">@overrides</span><span class="p">.</span><span class="nf">merge!</span><span class="p">(</span><span class="n">overrides</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>Its <code>camelize</code> method checks for the overrides and if it finds one, it uses it, otherwise it calls <code>String#camelize</code>
method, which is part of ActiveSupport core extensions for String.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="k">def</span> <span class="nf">camelize</span><span class="p">(</span><span class="n">first_letter</span> <span class="o">=</span> <span class="ss">:upper</span><span class="p">)</span>
<span class="k">case</span> <span class="n">first_letter</span>
<span class="k">when</span> <span class="ss">:upper</span>
<span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">camelize</span><span class="p">(</span><span class="nb">self</span><span class="p">,</span> <span class="kp">true</span><span class="p">)</span>
<span class="k">when</span> <span class="ss">:lower</span>
<span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">camelize</span><span class="p">(</span><span class="nb">self</span><span class="p">,</span> <span class="kp">false</span><span class="p">)</span>
<span class="k">else</span>
<span class="k">raise</span> <span class="no">ArgumentError</span><span class="p">,</span> <span class="s2">"Invalid option, use either :upper or :lower."</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>As you can see <code>String#camelize</code> delegates to <code>ActiveSupport::Inflector</code> under the hood.</p>
<p><code>ActiveSupport::Inflector</code> has been a part of Rails since the very beginning and is used to transform words from
singular to plural, class names to table names, modularized class names to ones without, and class names to foreign
keys.</p>
<p>However, in the context, of Zeitwerk, <strong>acronym handling</strong> is an essential feature of inflector.</p>
<p>An example of acronym is “REST” (Representational State Transfer). It is not uncommon to have a constant including it,
such as <code>API::REST::Client</code>.</p>
<p>When the classic autoloader encounters an undefined constant <code>API::REST::Client</code>, it
calls <code>API::REST::Client.to_s.underscore</code> to find the <code>api/rest/client.rb</code> file in the autoloaded directories.</p>
<p>On the other hand, Zeitwerk locates <code>api/rest/client.rb</code> and invokes <code>'api/rest/client'.camelize</code>. Without acronym
handling rules, this results in <code>Api::Rest::Client</code>. To get <code>API::REST::Client</code>, we need to supply an inflector with
acronym handling rules. In this post, I will demonstrate four distinct methods to accomplish that.</p>
<h2 id="1__configure_activesupport__inflector">1. Configure ActiveSupport::Inflector</h2>
<p>An intuitive and pretty common way is to configure <code>ActiveSupport::Inflector</code> directly.
But doing so affects how ActiveSupport inflects these phrases globally. It’s not always desired.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># config/initializers/inflections.rb</span>
<span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">inflections</span><span class="p">(</span><span class="ss">:en</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">inflect</span><span class="o">|</span>
<span class="n">inflect</span><span class="p">.</span><span class="nf">acronym</span> <span class="s1">'API'</span>
<span class="n">inflect</span><span class="p">.</span><span class="nf">acronym</span> <span class="s1">'REST'</span>
<span class="k">end</span>
</code></pre></div><h2 id="2__set_overrides_for_rails__autoloader__inflector">2. Set overrides for Rails::Autoloader::Inflector</h2>
<p>In some cases, you don’t want to add certain class or module naming rules to the ActiveSupport inflector.
It’s not mandatory.
You have the option to override particular inflections only for Zeitwerk and leave the Rails global inflector as it is.
However, even if you do that, Zeitwerk will still fall back to <code>String#camelize</code> and <code>ActiveSupport::Inflector</code> when it
cannot find a specific key.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># config/initializers/zeitwerk.rb</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">autoloaders</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">autoloader</span><span class="o">|</span>
<span class="n">autoloader</span><span class="p">.</span><span class="nf">inflector</span><span class="p">.</span><span class="nf">inflect</span><span class="p">(</span>
<span class="s2">"api"</span> <span class="o">=></span> <span class="s2">"API"</span><span class="p">,</span>
<span class="s2">"rest"</span> <span class="o">=></span> <span class="s2">"REST"</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">end</span>
</code></pre></div><h2 id="3__use_zeitwerk__inflector">3. Use Zeitwerk::Inflector</h2>
<p>Zeitwerk is a gem designed to be used independently from Rails and it provides an alternative implementation of
inflector that you can use instead of <code>Rails::Autoloader::Inflector</code>.
By doing so, you will have complete control over the acronyms you use in modules and classes naming conventions in a single place.
Furthermore, it will help you avoid polluting the ActiveSupport general-purpose inflector with autoloader-specific rules.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># config/initializers/zeitwerk.rb</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">autoloaders</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">autoloader</span><span class="o">|</span>
<span class="n">autoloader</span><span class="p">.</span><span class="nf">inflector</span> <span class="o">=</span> <span class="no">Zeitwerk</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">new</span>
<span class="n">autoloader</span><span class="p">.</span><span class="nf">inflector</span><span class="p">.</span><span class="nf">inflect</span><span class="p">(</span>
<span class="s2">"api"</span> <span class="o">=></span> <span class="s2">"API"</span><span class="p">,</span>
<span class="s2">"rest"</span> <span class="o">=></span> <span class="s2">"REST"</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">end</span>
</code></pre></div><h2 id="4__implement_your_custom_inflector">4. Implement your custom inflector</h2>
<p>Consider a scenario where, apart from the <code>API::REST::Client</code>, you also have the <code>User::Activities::Rest</code> constant in
your codebase. Both of them include the <code>/rest/i</code> substring, but you cannot use the same inflection rule to derive the
constant name from the file name.</p>
<p>This is a good example of when you may need to provide a custom inflector implementation.</p>
<p>Let’s revisit the standard <code>Rails::Autoloader::Inflector#camelize</code> method implementation to better understand this.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">camelize</span><span class="p">(</span><span class="n">basename</span><span class="p">,</span> <span class="n">_abspath</span><span class="p">)</span>
<span class="vi">@overrides</span><span class="p">[</span><span class="n">basename</span><span class="p">]</span> <span class="o">||</span> <span class="n">basename</span><span class="p">.</span><span class="nf">camelize</span>
<span class="k">end</span>
</code></pre></div>
<p>As you can see it is designed to take 2 arguments: <code>basename</code> and <code>_abspath</code>.
The <code>basename</code> is the file name without the extension and the <code>_abspath</code> is the absolute path to the file.</p>
<p>Note that the <code>_abspath</code> is not used in either the <code>Rails::Autoloader::Inflector</code> or the <code>Zeitwerk::Inflector</code>
implementation.</p>
<p>However, you can still take advantage of this argument presence in your custom implementation.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># config/initializers/zeitwerk.rb</span>
<span class="k">class</span> <span class="nc">UnconventionalInflector</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">conditional_inflection_for</span><span class="p">(</span><span class="n">basename</span><span class="p">:,</span> <span class="n">inflection</span><span class="p">:,</span> <span class="n">path</span><span class="p">:)</span>
<span class="no">Module</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="n">define_method</span> <span class="ss">:camelize</span> <span class="k">do</span> <span class="o">|</span><span class="n">basename_</span><span class="p">,</span> <span class="n">abspath</span><span class="o">|</span>
<span class="k">if</span> <span class="n">basename_</span> <span class="o">==</span> <span class="n">basename</span> <span class="o">&&</span> <span class="n">path</span><span class="p">.</span><span class="nf">match?</span><span class="p">(</span><span class="n">abspath</span><span class="p">)</span>
<span class="n">inflection</span>
<span class="k">else</span>
<span class="k">super</span><span class="p">(</span><span class="n">basename_</span><span class="p">,</span> <span class="n">abspath</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">prepend</span> <span class="n">conditional_inflection_for</span><span class="p">(</span>
<span class="ss">basename: </span><span class="s1">'rest'</span><span class="p">,</span>
<span class="ss">inflection: </span><span class="s1">'REST'</span><span class="p">,</span>
<span class="ss">path: </span><span class="sr">/\A</span><span class="si">#{</span><span class="no">Rails</span><span class="p">.</span><span class="nf">root</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">'lib'</span><span class="p">,</span> <span class="s1">'api'</span><span class="p">)</span><span class="si">}</span><span class="sr">/</span><span class="p">,</span>
<span class="p">)</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">initialize</span>
<span class="vi">@inflector</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">::</span><span class="no">Autoloader</span><span class="o">::</span><span class="no">Inflector</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">camelize</span><span class="p">(</span><span class="n">basename</span><span class="p">,</span> <span class="n">abspath</span><span class="p">)</span>
<span class="vi">@inflector</span><span class="p">.</span><span class="nf">camelize</span><span class="p">(</span><span class="n">basename</span><span class="p">,</span> <span class="n">abspath</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">inflect</span><span class="p">(</span><span class="n">overrides</span><span class="p">)</span>
<span class="vi">@inflector</span><span class="p">.</span><span class="nf">inflect</span><span class="p">(</span><span class="n">overrides</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">autoloaders</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">autoloader</span><span class="o">|</span>
<span class="n">autoloader</span><span class="p">.</span><span class="nf">inflector</span> <span class="o">=</span> <span class="no">UnconventionalInflector</span><span class="p">.</span><span class="nf">new</span>
<span class="n">autoloader</span><span class="p">.</span><span class="nf">inflector</span><span class="p">.</span><span class="nf">inflect</span><span class="p">(</span>
<span class="s1">'api'</span> <span class="o">=></span> <span class="s1">'API'</span>
<span class="p">)</span>
<span class="k">end</span>
</code></pre></div>
<p>The implementation above utilizes <code>Rails::Autoloader::Inflector</code> module. However, it prepends its <code>camelize</code>
implementation with the one that first checks if the file path matches an unconventional inflection rule.
If it does, the method uses an non-standard inflection. If not, it falls back to the default implementation.</p>
<hr>
<p>I understand that the example of <code>Rest</code> and <code>REST</code> may seem contrived, but it serves to illustrate the point. In
real-life situations, there may be more convincing reasons to implement a custom inflector, just as we did on a
project we were consulting, where it proved to be very helpful.</p>