The Ruby DispatchThe home for Rails-oriented content ranging from Turbo to Action Cable and more./ruby-dispatch/2024-03-05T00:00:00+00:00FlyExecute Third Party Code in a Rails Apphttps://fly.io/ruby-dispatch/execute-third-party-code-in-a-rails-app/2024-03-05T00:00:00+00:002024-03-15T00:52:24+00:00<p><strong class='font-[550] text-navy-950'>Imagine inviting random strangers from the internet to bring along their code and run it on your servers in a Rails app. Sounds like a security nightmare, doesn’t it? Where do you even start?</strong></p>
<p>If you run into a person at Fly.io, they might be saying something about “Fast booting VMs”, but what does that mean outside of faster deployment times?</p>
<p>Turns out when an entire machine can be boot in 2 seconds or less, it becomes possible to boot a server via a Rails background job, analyze a strangers code from within the confines of a virtual machine, and shut it down when the job is complete.</p>
<p>Sounds complicated right? It is, but Fly.io built the Machines API to manage all that complexity so you can spend your time and energy sweating the details about your app.</p>
<h2 id='the-problem' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-problem' aria-label='Anchor'></a>The Problem</h2>
<p>Inspecting or executing arbitrary code from third parties comes with a lot of risks.</p>
<p>First off, there’s a threat for the application that has to run it: It could be a target of exploits from malicious code being introduced by attackers to bring the app down, extract passwords, etc.</p>
<p>Then, there’s a risk for the customer of such an application: It could be exposed to malicious code of other, malevolent customers that targets extracting data or intellectual property.</p>
<p>The first is mainly a security issue for the application’s operation, while the latter is business critical as it undermines trust between customer and SaaS provider.</p>
<p>Luckily, Fly.io boasts a solution that provides a safe environment to deploy such workloads and is simple to manage: Fly Machines.</p>
<h2 id='the-context' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-context' aria-label='Anchor'></a>The Context</h2>
<p><a href='https://useattr.actor/' title=''>Attractor</a> is a code quality analysis tool that relies on the churn and complexity metrics to measure how tech debt evolves for a typical Ruby (on Rails) or JavaScript app.</p>
<p>At its heart lies a GitHub app that clones, inspects, (and optionally runs) third party code. A static analysis is conducted and the results are being reported back to the main app.</p>
<p>Since any paying customer can connect any GitHub repository, it would be possible to compromise the application (and customer data) were the user code cloned in the main app machine. A way to safely inspect and possibly run it had to be found.</p>
<h2 id='the-solution' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-solution' aria-label='Anchor'></a>The Solution</h2>
<p>Attractor is a Ruby on Rails app deployed on Fly.io with</p>
<ul>
<li>One process running the app server (puma)
</li><li>Two worker (sidekiq) queues: <code>default</code> and <code>sandbox</code>
</li><li>a Fly machines app to create and run machines on the fly. <strong class='font-[550] text-navy-950'>Important:</strong> make sure you run this app in a private network for true isolation, as pointed out <a href='https://fly.io/docs/machines/guides-examples/functions-with-machines/#create-an-app' title=''>here</a>.
</li></ul>
<p>When new code changes come in via a pull request, it uses a <code>SandboxRun</code> model to encapsulate such a workload:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-s34vwxdg"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-s34vwxdg"><span class="k">class</span> <span class="nc">SandboxRun</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">has_secure_token</span>
<span class="n">belongs_to</span> <span class="ss">:github_pull_request</span><span class="p">,</span> <span class="ss">class_name: </span><span class="s2">"Github::PullRequest"</span>
<span class="n">after_create_commit</span> <span class="ss">:start</span>
<span class="k">def</span> <span class="nf">invalidate</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">invalidated_at</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">utc</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">invalidate!</span>
<span class="n">invalidate</span>
<span class="n">save!</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">invalidated?</span>
<span class="o">!!</span><span class="n">invalidated_at</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">start</span>
<span class="k">return</span> <span class="k">if</span> <span class="n">invalidated?</span>
<span class="no">SandboxRunJob</span><span class="p">.</span><span class="nf">perform_later</span><span class="p">(</span><span class="nb">self</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>After a <code>SandboxRun</code> record is created, it self-executes via a <code>SandboxRunJob</code>:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-acg03wjw"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-acg03wjw"><span class="k">class</span> <span class="nc">SandboxRunJob</span> <span class="o"><</span> <span class="no">ApplicationJob</span>
<span class="n">queue_as</span> <span class="ss">:sandbox</span>
<span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">sandbox_run</span><span class="p">)</span>
<span class="vi">@sandbox_run</span> <span class="o">=</span> <span class="n">sandbox_run</span>
<span class="n">boot_sandbox</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">boot_sandbox</span>
<span class="n">res_create</span> <span class="o">=</span> <span class="n">conn</span><span class="p">.</span><span class="nf">post</span> <span class="s2">"apps/my-app-machines/machines"</span><span class="p">,</span>
<span class="s2">"{
</span><span class="se">\"</span><span class="s2">name</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">sandbox-machine-</span><span class="si">#{</span><span class="vi">@sandbox_run</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="se">\"</span><span class="s2">,
</span><span class="se">\"</span><span class="s2">config</span><span class="se">\"</span><span class="s2">: {
</span><span class="se">\"</span><span class="s2">image</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">my-sandbox-image:latest</span><span class="se">\"</span><span class="s2">,
</span><span class="se">\"</span><span class="s2">guest</span><span class="se">\"</span><span class="s2">: {
</span><span class="se">\"</span><span class="s2">memory_mb</span><span class="se">\"</span><span class="s2">: 512,
</span><span class="se">\"</span><span class="s2">cpu_kind</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">shared</span><span class="se">\"</span><span class="s2">,
</span><span class="se">\"</span><span class="s2">cpus</span><span class="se">\"</span><span class="s2">: 1
},
</span><span class="se">\"</span><span class="s2">restart</span><span class="se">\"</span><span class="s2">: {
</span><span class="se">\"</span><span class="s2">policy</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">no</span><span class="se">\"</span><span class="s2">
},
</span><span class="se">\"</span><span class="s2">env</span><span class="se">\"</span><span class="s2">: {
</span><span class="se">\"</span><span class="s2">SANDBOX_RUN_ID</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="si">#{</span><span class="vi">@sandbox_run</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="se">\"</span><span class="s2">,
</span><span class="se">\"</span><span class="s2">SANDBOX_RUN_TOKEN</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="si">#{</span><span class="vi">@sandbox_run</span><span class="p">.</span><span class="nf">token</span><span class="si">}</span><span class="se">\"</span><span class="s2">,
}
}
}"</span><span class="p">,</span>
<span class="s2">"Content-Type"</span> <span class="o">=></span> <span class="s2">"application/json"</span>
<span class="c1"># abort processing if machine start failed</span>
<span class="k">if</span> <span class="n">res_create</span><span class="p">.</span><span class="nf">status</span> <span class="o">>=</span> <span class="mi">400</span>
<span class="k">raise</span> <span class="no">SandboxStartupError</span><span class="p">,</span> <span class="n">res_create</span><span class="p">.</span><span class="nf">body</span><span class="p">[</span><span class="s2">"error"</span><span class="p">]</span>
<span class="k">end</span>
<span class="vi">@sandbox_run</span><span class="p">.</span><span class="nf">fly_machine_id</span> <span class="o">=</span> <span class="n">res_create</span><span class="p">.</span><span class="nf">body</span><span class="p">[</span><span class="s2">"id"</span><span class="p">]</span>
<span class="vi">@sandbox_run</span><span class="p">.</span><span class="nf">save</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">conn</span>
<span class="vi">@conn</span> <span class="o">||=</span> <span class="no">Faraday</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
<span class="ss">url: </span><span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s2">"FLY_API_URL"</span><span class="p">,</span> <span class="s2">"http://_api.internal:4280/v1"</span><span class="p">)</span>
<span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">conn</span><span class="o">|</span>
<span class="n">conn</span><span class="p">.</span><span class="nf">request</span> <span class="ss">:authorization</span><span class="p">,</span> <span class="s2">"Bearer"</span><span class="p">,</span> <span class="no">ENV</span><span class="p">[</span><span class="s2">"FLY_API_TOKEN"</span><span class="p">]</span>
<span class="n">conn</span><span class="p">.</span><span class="nf">response</span> <span class="ss">:json</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>This job boots a sandbox by issuing a <code>POST</code> request to the Fly machines app (<code>my_app_machines</code>). It spawns a container using a Docker image (<code>my-sandbox-image:latest</code>) that has to be present in your organization’s registry. Furthermore it is passed two environment variables (<code>SANDBOX_RUN_ID</code> and <code>SANDBOX_RUN_TOKEN</code>) to identify the sandbox run. Critically, the restart policy is set to <code>no</code> to avoid infinite loops.</p>
<p>The logic that runs in the actual sandbox is secondary, it simply returns a JSON payload in form of a <code>POST</code> request to an incoming webhooks controller:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-5t0pb6c"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-5t0pb6c"><span class="k">class</span> <span class="nc">SandboxWebhooksController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="c1"># some details omitted</span>
<span class="n">before_action</span> <span class="ss">:authenticate_token!</span>
<span class="k">def</span> <span class="nf">create</span>
<span class="no">SandboxWebhook</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">data: </span><span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="nf">body</span><span class="p">.</span><span class="nf">read</span><span class="p">)).</span><span class="nf">process_async</span>
<span class="n">render</span> <span class="ss">json: </span><span class="p">{</span><span class="ss">status: </span><span class="s2">"OK"</span><span class="p">},</span> <span class="ss">status: :created</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">authenticate_token!</span>
<span class="vi">@sandbox_run</span> <span class="o">||=</span> <span class="n">sandbox_run_from_token</span>
<span class="n">head</span> <span class="ss">:unauthorized</span> <span class="k">unless</span> <span class="vi">@sandbox_run</span><span class="p">.</span><span class="nf">present?</span> <span class="o">&&</span> <span class="o">!</span><span class="vi">@sandbox_run</span><span class="p">.</span><span class="nf">invalidated?</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">sandbox_run_from_token</span>
<span class="no">SandboxRun</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">token: </span><span class="n">token_from_header</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">token_from_header</span>
<span class="n">request</span><span class="p">.</span><span class="nf">headers</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s2">"Authorization"</span><span class="p">,</span> <span class="s2">""</span><span class="p">).</span><span class="nf">split</span><span class="p">(</span><span class="s2">" "</span><span class="p">).</span><span class="nf">last</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Note that the sandbox run is authenticated via a unique secure token that we passed to the sandbox machine as an environment variable (<code>SANDBOX_RUN_TOKEN</code>). Optionally, precautions can be made to make this endpoint only accessible from the internal network.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-sjzxxxho"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-sjzxxxho"><span class="k">class</span> <span class="nc">SandboxWebhook</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="c1"># module includes omitted</span>
<span class="k">def</span> <span class="nf">process</span>
<span class="vi">@sandbox_run</span> <span class="o">=</span> <span class="no">SandboxRun</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s2">"sandbox_run"</span><span class="p">][</span><span class="s2">"id"</span><span class="p">])</span>
<span class="k">return</span> <span class="k">if</span> <span class="vi">@sandbox_run</span><span class="p">.</span><span class="nf">invalidated?</span>
<span class="c1"># process incoming payload</span>
<span class="vi">@sandbox_run</span><span class="p">.</span><span class="nf">invalidate!</span>
<span class="k">ensure</span>
<span class="n">teardown_sandbox</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">teardown_sandbox</span>
<span class="n">_res_wait</span> <span class="o">=</span> <span class="n">conn</span><span class="p">.</span><span class="nf">get</span> <span class="s2">"apps/my-app-machines/machines/</span><span class="si">#{</span><span class="vi">@sandbox_run</span><span class="p">.</span><span class="nf">fly_machine_id</span><span class="si">}</span><span class="s2">/wait"</span><span class="p">,</span>
<span class="p">{</span>
<span class="ss">state: </span><span class="s2">"stopped"</span><span class="p">,</span>
<span class="ss">instance_id: </span><span class="n">machine_instance_id</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="s2">"Content-Type"</span> <span class="o">=></span> <span class="s2">"application/json"</span>
<span class="p">}</span>
<span class="n">res_delete</span> <span class="o">=</span> <span class="n">conn</span><span class="p">.</span><span class="nf">delete</span> <span class="s2">"apps/my-app-machines/machines/</span><span class="si">#{</span><span class="vi">@sandbox_run</span><span class="p">.</span><span class="nf">fly_machine_id</span><span class="si">}</span><span class="s2">"</span>
<span class="k">if</span> <span class="n">res_delete</span><span class="p">.</span><span class="nf">status</span> <span class="o">>=</span> <span class="mi">400</span>
<span class="k">raise</span> <span class="no">SandboxShutdownError</span><span class="p">,</span> <span class="n">res_delete</span><span class="p">.</span><span class="nf">body</span><span class="p">[</span><span class="s2">"error"</span><span class="p">]</span>
<span class="k">end</span>
<span class="n">res_delete</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">machine_instance_id</span>
<span class="n">res_machine</span> <span class="o">=</span> <span class="n">conn</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="s2">"apps/my-app-machines/machines/</span><span class="si">#{</span><span class="vi">@sandbox_run</span><span class="p">.</span><span class="nf">fly_machine_id</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="n">res_machine</span><span class="p">.</span><span class="nf">body</span><span class="p">[</span><span class="s2">"instance_id"</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>In the created <code>SandboxWebhook</code> model the actual payload processing takes place, which isn’t really of interest. We have to take care, though, that the corresponding sandbox run is invalidated so it doesn’t get executed a second time.</p>
<p>The more salient part of this model for the purposes of this article is the <em>tearing down</em> of the sandbox machine. We want to clean up after the sandbox has run, otherwise we would have dangling machines that add to our bill. To destroy a machine, we have to wait for it to become <code>stopped</code>, though. This is done via a special <code>/wait</code> endpoint that we pass the desired state and the machine’s instance ID.</p>
<p><strong class='font-[550] text-navy-950'>Beware:</strong> This is different from the machine’s ID, which is why we have to invoke another endpoint to obtain it.</p>
<p>The response to the <code>/wait</code> call blocks until the machine reaches the desired state. Afterwards we can destroy it, and re-raise any possibly resulting error.</p>
<h2 id='wrap-up' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#wrap-up' aria-label='Anchor'></a>Wrap-up</h2>
<p>Solving the need to separate user code from our own application, we picked up Fly Machines to run ephemeral, isolated workloads. We’ve shown a way to integrate these sandboxes and the results they produce in an idiomatic Rails workflow. In the future, hopefully the verbosity of the API integration will be replaced by an official Fly SDK to create, start, and destroy machines.</p>
/ruby-dispatch/execute-third-party-code-in-a-rails-app/assets/cover.webpThe Plan for Rails 8https://fly.io/ruby-dispatch/the-plan-for-rails-8/2024-02-05T00:00:00+00:002024-02-06T19:54:17+00:00<p><strong class='font-[550] text-navy-950'>Rails published an <a href='https://github.com/rails/rails/milestone/87' title=''>8.0 milestone</a> on Github that lays out the goals for the next Rails release, and it’s sprawling. It ships with a new background worker, cache backend, asset pipeline, framework for pushing notifications to mobile devices, and a load of development tools.</strong></p>
<p>One of Rails’ mantras is “compress the complexity of modern web apps”, which is a technical way of saying “make it easier to build web applications”. Rails 8 continues this tradition by reducing infrastructure and “moving parts” required to run Rails in production. It’s also taking on the App Store Industrial Complex by making it easier for Rails developers to ship progressive web applications with features that rival native mobile apps.</p>
<h2 id='less-moving-parts-in-production' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#less-moving-parts-in-production' aria-label='Anchor'></a>Less moving parts in production</h2>
<p><a href='https://redis.io' title=''>Redis</a> is a fast, reliable piece of software that has been a staple of the Rails stack for years. Despite its stability, Redis remains a dependency that requires monitoring and can fail. Have you heard of key eviction? You won’t have to find out what that means if you ship a default Rails 8 app.</p>
<p>Rails 8 reduces its dependencies on Redis by shifting more workload from Redis into the database on the premise that they’re getting much faster, thanks to NVMe solid state drives.</p>
<h3 id='solid-cache-amp-queue' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#solid-cache-amp-queue' aria-label='Anchor'></a>Solid Cache & Queue</h3>
<p><a href='https://github.com/rails/solid_cache' title=''>Solid Cache</a> and <a href='https://github.com/basecamp/solid_queue' title=''>Solid Queue</a> are two new software libraries that ship with Rails 8. They’re both built on top of the database and are designed to replace libraries that use Redis for caching and background jobs.</p>
<p>Are they as fast as a Redis backed worker queue and cache? Nope, generally they’re slower because the database needs to commit data to disk whereas Redis stores it in memory, but they are “fast enough” to handle caching and worker queue workloads for most Rails apps because database disks are much faster thanks to NVMe solid state drives.</p>
<p>There’s still a need for Redis in some cases, like rate limiting HTTP requests, but for the most part, you can get away with not using Redis at all in a production Rails deployment.</p>
<h3 id='database-backed-actioncable-websocket-broadcasting' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#database-backed-actioncable-websocket-broadcasting' aria-label='Anchor'></a>Database backed ActionCable WebSocket broadcasting</h3>
<p><a href='https://www.postgresql.org/docs/current/sql-notify.html' title=''>Postgres</a> and <a href='https://dev.mysql.com/blog-archive/mysql-8-0-1-using-skip-locked-and-nowait-to-handle-hot-rows/' title=''>MySQL</a> ship with features that make it possible to broadcast messages to web browsers over WebSockets. Rails 8 will ship with a new ActionCable adapter that uses the database to broadcast messages to all connected clients which eliminates another Redis dependency.</p>
<p>Since databases weren’t designed with pub/sub as their primary use case, there can be limitations. For example, Postgres has a <a href='https://www.postgresql.org/docs/current/sql-notify.html' title=''>default maximum payload size of 8000 bytes</a>, which could be a problem for an application trying to push a large HTML payload over the wire.</p>
<p>Fortunately Rails 8 also ships with <a href='https://fly.io/ruby-dispatch/turbo-8-in-8-minutes/' title=''>Turbo 8</a> page morphs, which is unlikely to reach the default payload size limits since it only publishes the fact that a model has been changed and the server should reload the HTML over HTTP.</p>
<h2 id='propshaft-replaces-sprockets' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#propshaft-replaces-sprockets' aria-label='Anchor'></a>Propshaft replaces Sprockets</h2>
<p>Web applications need to quickly & efficiently serve up CSS, JavaScript, and image assets. One of the most effective methods of doing this is to “fingerprint” an asset and serving it up by its fingerprint with a cache header that never expires.</p>
<p>That’s exactly what <a href='https://github.com/rails/propshaft' title=''>Propshaft</a> does: it digests the image at <code>/images/foo.png</code> and generates a URL like <code>/images/foo-586e0b396456675e08bba21db5941a3099b07766084e51400576f7622c5217cc.png</code> with a cache header that effectively never expires.</p>
<p><a href='https://github.com/rails/sprockets' title=''>Sprockets</a> was the original Rails asset pipeline included into Rails in 2009 that predates JS-based asset pipelines like web pack. Consequently Sprockets had to do all the heavy lifting that asset pipelines like esbuild do today like compiling SCSS into CSS, minifying JavaScript, etc.</p>
<p>It’s 2024 and we now live in a world where most browsers can efficiently download JavaScript and stylesheet assets without an asset pipeline or a more capable pipeline can be used like <a href='https://esbuild.github.io' title=''>esbuild</a>. The only thing Rails needs to do is generate fingerprinted URLs for images, videos, and other media files. That’s why Propshaft was introduced—it gets out of the business of CSS and JavaScript compilation and leaves that up to either modern browsers or more capable asset compilers like esbuild.</p>
<h2 id='ship-progressive-web-apps-instead-of-native-apps' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#ship-progressive-web-apps-instead-of-native-apps' aria-label='Anchor'></a>Ship progressive web apps instead of native apps</h2>
<p>macOS Sonoma, iOS 17, and iPadOS 17 <a href='https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/' title=''>shipped with “progressive web application” features</a> that make it possible for web applications to integrate better with operating system without the need for a native app.</p>
<h3 id='push-notifications' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#push-notifications' aria-label='Anchor'></a>Push notifications</h3>
<p>One big reason to ship native applications was for notifications. For example, a Rails chat app might need to notify a user if they’re mentioned with @user so they can respond to a message.</p>
<p>The most recent version of Safari finally <a href='https://developer.apple.com/documentation/usernotifications/sending_web_push_notifications_in_web_apps_and_browsers?language=objc' title=''>introduced the web notifications feature</a> making it feasible for Rails to bake it into the framework. That means mobile web notifications are possible <em>without</em> installing a native iOS application.</p>
<p>It’s not clear yet if web notifications will use an existing gem or if the Rails team will roll their own.</p>
<h3 id='add-a-web-page-to-the-home-screen-or-dock' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#add-a-web-page-to-the-home-screen-or-dock' aria-label='Anchor'></a>Add a web page to the Home Screen or dock</h3>
<p>Improvements have been made to adding a webpage to the Home Screen on iPhones and iPads and now it’s possible to <a href='https://support.apple.com/en-us/104996' title=''>add a webpage to the dock so that it appears as an app</a>.</p>
<p>This has been on Windows and Android for a while, but it’s nice to see Apple finally offer decent support for it making it a viable channel for distributing apps.</p>
<h2 id='lots-and-lots-of-development-tools' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#lots-and-lots-of-development-tools' aria-label='Anchor'></a>Lots and lots of development tools</h2>
<p>Rails 8 ships with a lot of additional default tools that make it easier to develop and deploy Rails applications.</p>
<h3 id='deployments-with-kamal' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#deployments-with-kamal' aria-label='Anchor'></a>Deployments with Kamal</h3>
<p><a href='https://kamal-deploy.org' title=''>Kamal</a> gets anointed as a default when running <code>rails new</code>. Kamal is a deployment tool that makes deploying Rails applications to metal servers possible if you’re comfortable managing a load balancer, HTTPS certificates, Docker, and more.</p>
<p>You don’t need Kamal to deploy to Fly.io since we provide you with the load balancer and tooling needs to handle HTTP certificates.</p>
<h3 id='http-basic-authentication-generator' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#http-basic-authentication-generator' aria-label='Anchor'></a>HTTP Basic Authentication generator</h3>
<p>Rails 8 will ship with a code generator that will add HTTP Basic authentication to Rails applications. This has been possible already with third-party gems or a few lines of Ruby code, so this will be a nice addition for those who are new to the framework and need more implementation guidance.</p>
<h3 id='benchmarking-tool' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#benchmarking-tool' aria-label='Anchor'></a>Benchmarking tool</h3>
<p>A common benchmarking tool will make limited apples-to-apples comparison possible between hosts. The purpose of these benchmarks isn’t to understand how a specific application will behave in production—rather its a suite that tests disk speed, networking, and other aspects of server performance that can help developers make a more informed decision about where to deploy their Rails apps.</p>
<h3 id='rails-8-language-server' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#rails-8-language-server' aria-label='Anchor'></a>Rails 8 language server</h3>
<p>Language servers instruct IDEs, like Zed Editor and VS Code, what to display in the autocomplete pop-over while coding. Rails 8 will ship with a language server that will offer autocomplete for Rails constructs like ActiveRecord validations, etc.</p>
<h3 id='rubocop' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#rubocop' aria-label='Anchor'></a>RuboCop</h3>
<p>For those of you who like linters, Rails 8 is going to ship with <a href='https://github.com/rails/rubocop-rails-omakase' title=''>RuboCop Omakase</a>. You’ll be able to customize it to whatever flavor of Ruby you or your team prefers.</p>
<h3 id='github-integration' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#github-integration' aria-label='Anchor'></a>Github integration</h3>
<p>Rails 8 projects now have a <code>.github</code> folder with everything configured for running CI, pull request templates, and other common workflows on Github.</p>
<h3 id='devcontainers' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#devcontainers' aria-label='Anchor'></a>Devcontainers</h3>
<p><a href='https://containers.dev/' title=''>Devcontainers</a> are a set of scripts that setup a development environment on your local workstations via Dockerfiles and a configuration file.</p>
<p>Be aware on macOS you may run into issues in larger Rails projects with lots of files where Docker Desktop’s hypervisor performance slows down your development environment by an order of magnitude.</p>
<h2 id='better-http-2-support' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#better-http-2-support' aria-label='Anchor'></a>Better HTTP/2 support</h2>
<p>Rails continues its slow march towards better HTTP/2 support with a few upgrades.</p>
<h3 id='rack-3' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#rack-3' aria-label='Anchor'></a>Rack 3</h3>
<p>Rack is a low level HTTP API that sits between Ruby web frameworks, like Rails, and web application servers, like Puma and Falcon. <a href='https://github.com/rack/rack/blob/main/UPGRADE-GUIDE.md' title=''>Rack 3 ships with better APIs</a> for HTTP/2 that cleans up some of the hacks that were required to make it work in previous versions of Rack.</p>
<h3 id='http-2-in-thruster' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#http-2-in-thruster' aria-label='Anchor'></a>HTTP/2 in Thruster</h3>
<p>Thruster is a Dockerfile for Rails apps that includes an HTTP/2 proxy for web hosts that don’t support it out of the box. If you deploy your app to Fly.io, you won’t need this since Fly’s applicaton proxy supports HTTP/2 with no additional configuration.</p>
<h2 id='documentation-for-activemodel' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#documentation-for-activemodel' aria-label='Anchor'></a>Documentation for ActiveModel</h2>
<p>ActiveModel is an API that’s been extracted from ActiveRecord that makes it possible to make plain ‘ol Ruby objects behave more like ActiveRecord models. It’s been around forever, but now it finally <a href='https://github.com/rails/rails/issues/50568' title=''>gets the documentation it deserves</a>
in Rails 8.</p>
<h2 id='wrap-up' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#wrap-up' aria-label='Anchor'></a>Wrap-up</h2>
<p>Rails 8 is shaping up to be a big release that will significantly expand the default tooling included with a Rails app and introduce new frameworks that eliminate the need for production service dependencies like Redis. As always, you’ll still be able to remove whatever pieces you don’t like when creating a new Rails app or pulling the gems out later.</p>
<p>Overall most of the features delivered in Rails 8 will delay the need to think about production infrastructure early on in your app. This makes Rails more approachable for people new to web development since they have fewer decisions to make.</p>
/ruby-dispatch/the-plan-for-rails-8/assets/cover.webpRails on Docker without Dockerhttps://fly.io/ruby-dispatch/rails-on-docker-without-docker/2024-01-22T00:00:00+00:002024-01-24T19:32:24+00:00<p><strong class='font-[550] text-navy-950'>Docker will play a major role in the future of Rails production deployments, but not everybody wants to learn Docker, understand Linux systems administration, and install additional tooling on their workstations. Fortunately Fly.io can do this heavy lifting for you so you can focus on building applications and ease into these details later when your application demands it.</strong></p>
<p>During Rails World 2023, several new parts of the Rails stack were announced including an official Dockerfile would be included in Rails 7.1 and a new deployment tool, Kamal, that uses Docker to package up and deploy Rails applications to servers.</p>
<p>There’s just one problem: not all Rails developers want to install Docker on their workstations and learn how to craft a Dockerfile & all the Linux commands that go along with it. The good news is that Fly.io does all that for you so you can focus on building your app and worry about all that stuff later. Let’s see how.</p>
<h2 id='rails-developers-dont-have-to-be-linux-system-administrators-or-docker-experts-to-deploy-to-production' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#rails-developers-dont-have-to-be-linux-system-administrators-or-docker-experts-to-deploy-to-production' aria-label='Anchor'></a>Rails developers don’t have to be Linux system administrators or Docker experts to deploy to production</h2>
<p>A <a href='https://fly.io/ruby-dispatch/rails-on-docker/' title=''>Dockerfile</a> is a text file with a bunch of commands that copy files, install packages, install gems, execute Linux commands, and describe the command that launches the app. These files are read by a builder that downloads the necessary Linux operating system, packages, and source code and bundles them all up in an image. This image is then sent to Fly.io and run on top of bare metal servers via <a href='https://firecracker-microvm.github.io/' title=''>Firecracker virtual machines</a>, which is really fast.</p>
<p>For people who are comfortable and experienced managing Linux systems, Docker is relatively easy to learn, but many developers aren’t familiar with Linux and just want to build and ship great apps on a platform that Just Works™.</p>
<p>Fly.io created and open-sourced the <a href='https://github.com/fly-apps/dockerfile-rails' title=''><code>dockerfiles-rails</code> gem</a> to make it easier for beginners and experts to create Dockerfiles for their apps. It looks at the gems and dependencies of Rails apps and automatically creates a Dockerfile with the configuration needed to run in production.</p>
<h3 id='the-dockerfile-rails-gem-automatically-generates-reasonable-dockerfiles-so-you-dont-have-to' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-dockerfile-rails-gem-automatically-generates-reasonable-dockerfiles-so-you-dont-have-to' aria-label='Anchor'></a>The <code>dockerfile-rails</code> gem automatically generates reasonable Dockerfiles so you don’t have to</h3>
<p>Want to quickly create secure Rails app Dockerfiles for production? Good news! You don’t have to go searching through 20 tutorials on the internet to piece something together—instead you can run these commands from the root of your Rails project:</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-f4aecl2j"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-f4aecl2j">bundle add dockerfile-rails <span class="nt">--optimistic</span> <span class="nt">--group</span> development
bin/rails generate dockerfile
</code></pre>
</div>
</div>
<p>This gem looks at the <code>Gemfile</code> and <code>package.json</code> files, if present, in your Rails application folder to determine what operating system packages should be installed. The gem is capable of automating some pretty gnarly deployments ranging from installing vips for imaging processing to getting Puppeteer working to take screenshots of webpages… all with one command: <code>rails g dockerfile</code>.</p>
<h3 id='automatically-generate-dockerfiles-for-older-rails-apps' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#automatically-generate-dockerfiles-for-older-rails-apps' aria-label='Anchor'></a>Automatically generate Dockerfiles for older Rails apps</h3>
<p>Rails 7.1 ships with a default Dockerfile, which is great, but what about that old Rails 6 app you want to run on a Docker host that you don’t plan on upgrading for a while? Yep, the <code>dockerfile-rails</code> gem will happily create a Dockerfile for it so you can deploy it to any Docker-compatible host like Fly.io.</p>
<h3 id='consolidate-docker-knowledge-into-one-gem-instead-of-random-internet-tutorials' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#consolidate-docker-knowledge-into-one-gem-instead-of-random-internet-tutorials' aria-label='Anchor'></a>Consolidate Docker knowledge into one gem instead of random internet tutorials</h3>
<p>Prior to the <code>dockerfile-rails</code> gem, creating Dockerfiles usually consisted of searching the internet for tutorials showing you how to do it. After hours of hacking together a FrankenDockerfile you might end up with something shippable, but it probably isn’t ideal or as secure as it should be.</p>
<p>The <code>dockerfile-rails</code> gem consolidates and automates all of this knowledge in one place. As more people in the Rails community use it and open PR’s to integrate their use cases, we all benefit and updating that knowledge becomes a matter of running:</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-hzh159r8"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-hzh159r8">bundle update
bin/rails g dockerfile
</code></pre>
</div>
</div>
<p>If you’re up for it, you can <a href='https://github.com/fly-apps/dockerfile-rails' title=''>open a pull request on the dockerfile-rails repository</a> and commit your Dockerfile configurations.</p>
<h2 id='run-less-stuff-on-development-workstations' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#run-less-stuff-on-development-workstations' aria-label='Anchor'></a>Run less stuff on development workstations</h2>
<p>For Rails developers who are dedicated to having minimal tooling on their machines, Fly.io makes it possible to use Docker without running Docker on your workstation with its remote builder. This offers several benefits:</p>
<h3 id='deploy-over-crappy-internet-connections' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#deploy-over-crappy-internet-connections' aria-label='Anchor'></a>Deploy over crappy internet connections</h3>
<p>When a Rails app is built in Docker from a development workstation it has to download the Linux operating system and packages, build the image, then push everything back over the wire to your servers. This can take a really long time over a slow internet connection, especially if it frequently disconnects.</p>
<p>With Fly.io, the Docker builder runs on Fly’s infrastructure. That means only the application source code is copied from the development workstation to the build server, which is usually a heck of a lot less data to move over the wire than an entire Linux distribution and all of its packages.</p>
<p>Fly’s hosted builder has saved my bacon countless times when I’m on a crappy WiFi connection that barely works and I need to deploy something quickly to production, like this deployment from my local purveyor hopped beverages.</p>
<p><img alt="Deploying an application from the Canyon Club's WiFi network." src="/images/questionable-wifi.jpg" /></p>
<h3 id='developers-that-use-macos-apple-silicon-dont-have-to-emulate-an-intel-docker-builder' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#developers-that-use-macos-apple-silicon-dont-have-to-emulate-an-intel-docker-builder' aria-label='Anchor'></a>Developers that use macOS Apple Silicon don’t have to emulate an Intel Docker builder</h3>
<p>Intel Linux servers are still the most dominant processor architecture in production environments. That means developers running Apple Silicon or ARM workstations have to emulate Intel processors on their local workstations to build a Docker image that will run on Intel production servers, which can be slow.</p>
<p>Since Fly.io runs Docker builders on Intel servers all a developer workstation needs to do is copy the code files over the wire to the build server. The build server builds the image on native Intel hardware, pushes it up to the image repository, then deploys it to Fly.io servers around the world when <code>fly deploy</code> is run.</p>
<h2 id='what-about-kamal' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#what-about-kamal' aria-label='Anchor'></a>What about Kamal?</h2>
<p>Kamal is a great way to deploy Rails applications to a wide variety of hosts ranging from bare metal servers to virtual servers, but it does require that you learn more about Docker, know a few things about Linux & networking, manage a load balancer, and install more software on your workstation like Docker.</p>
<h3 id='you-should-use-dockerfile-rails-with-kamal' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#you-should-use-dockerfile-rails-with-kamal' aria-label='Anchor'></a>You should use <code>dockerfile-rails</code> with Kamal</h3>
<p>The gem is completely agnostic production servers, which means you can use it with Kamal to generate Dockerfiles for your Rails apps.</p>
<h3 id='kamal-requires-you-to-bring-your-own-docker-builder-and-image-repository' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#kamal-requires-you-to-bring-your-own-docker-builder-and-image-repository' aria-label='Anchor'></a>Kamal requires you to bring your own Docker builder and image repository</h3>
<p>It’s no secret that Kamal is the more manual way of running your infrastructure including managing your own certificates, load balancer, and more. Fly.io manages SSL certificates, load balancing, and Docker for you and only needs your application’s Dockerfile.</p>
<h3 id='kamal-and-dockerfile-rails-makes-it-easier-to-switch-hosts' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#kamal-and-dockerfile-rails-makes-it-easier-to-switch-hosts' aria-label='Anchor'></a>Kamal and <code>dockerfile-rails</code> makes it easier to switch hosts</h3>
<p>Getting a Rails app running in Docker is the hard part, but once that’s accomplished its possible to deploy a Rails app to more production targets via Kamal, Kubernetes, or Fly.io via <code>fly deploy</code>. This means its easier to switch hosting providers in the future should the need arise.</p>
<h3 id='can-i-use-kamal-with-fly-io' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#can-i-use-kamal-with-fly-io' aria-label='Anchor'></a>Can I use Kamal with Fly.io?</h3>
<p>Yes, but it’s not necessary to use Kamal since Fly.io manages the load balancer, Docker builds, and restarts for you via the <code>fly deploy</code> command.</p>
<h2 id='fly-launch-uses-dockerfile-rails-to-configure-rails-applications' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#fly-launch-uses-dockerfile-rails-to-configure-rails-applications' aria-label='Anchor'></a><code>fly launch</code> uses <code>dockerfile-rails</code> to configure Rails applications</h2>
<p>When you launch a new Rails app on Fly.io the following happens:</p>
<ol>
<li><code>fly launch</code> detects a <code>Gemfile</code> in the root of a project with a <code>rails</code> gem dependency.
</li><li><code>fly launch</code> detects the absence of a <code>Dockerfile</code> and runs <code>bundle add dockerfile-rails</code> at the root of the project, then runs <code>rails generate dockerfile</code>.
</li><li>The <code>rails g dockerfile</code> command scans your <code>Gemfile</code> and <code>package.json</code> to install the necessary Linux packages. For example, the <code>puppeteer</code> gem will install headless Chrome.
</li></ol>
<p>After Fly.io virtual machines are provisioned when running <code>fly launch</code>, the <code>fly deploy</code> command will use the remote Fly.io Docker builder to build the Docker image, push it to the servers, and get your app up and running.</p>
<p><a href='https://fly.io/docs/rails/' title=''>Read through the Rails on Fly.io documentation</a> for more details on how to lunch your Rails app on Fly.io.</p>
<h2 id='dont-be-afraid-of-linux-and-docker' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#dont-be-afraid-of-linux-and-docker' aria-label='Anchor'></a>Don’t be afraid of Linux and Docker</h2>
<p>It might seem unnecessary to have a Dockerfile in the root of your application when you’re starting out, but there will be a day where you add something different to your Rails application that requires a specific Linux package. When this happens you’ll be glad you can open the Dockerfile, make your change, and deploy.</p>
<p>Fly.io and the <code>dockerfile-rails</code> will gradually introduce you to Docker and Linux concepts in a way that allow you to be immediately productive.</p>
<h2 id='wrap-up' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#wrap-up' aria-label='Anchor'></a>Wrap-up</h2>
<p>The Rails community has embraced Dockerfile as its “unit of deployment”, which will make it easier to deploy Rails apps to more hosts that support Docker including your own servers via Kamal or to managed services like Fly.io.</p>
<p>For those who don’t want to stop work on their Rails apps to learn Docker, Kamal, & Linux commands, Fly.io will do the heavy lifting of creating a Dockerfile for your Rails app, deploying it to production behind a load balancer, handle SSL certificates, and host the Docker builder so you don’t have to install it on your workstation.</p>
/ruby-dispatch/rails-on-docker-without-docker/assets/cover.webp8 Turbo 8 "Gotchas"https://fly.io/ruby-dispatch/8-turbo-8-gotchas/2024-01-02T00:00:00+00:002024-01-08T20:13:24+00:00<p><strong class='font-[550] text-navy-950'>As the final release of Turbo 8 nears, there are a few “gotchas” worth knowing about that will make life easier as you start updating existing Rails Turbo apps to take advantage of all the new features that Turbo 8 offers, like page morphs</strong></p>
<h2 id='1-dont-scroll-preserve-all-the-things' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#1-dont-scroll-preserve-all-the-things' aria-label='Anchor'></a>1. Don’t <code>scroll: preserve</code> All The Things™</h2>
<p>There are two <code>scroll</code> properties in Turbo 8 that instruct the browser how to behave when users click on a link or submit a form: the new <code>preserve</code> property and the existing <code>reset</code> property.</p>
<p>The <code>reset</code> property is the default behavior and it’s what Turbo has always been using. It starts pages at the top of the viewport when the user navigates after clicking a link. Nothing too surprising there—it’s how web browsers have worked for decades.</p>
<p>The <code>preserve</code> property is the new kid on the block that works a bit differently. When the page loads the viewport stays put. That means if this property is mistakenly set on a content page and the user clicks on a link in the footer, they would continue seeing the footer as the content above gets updated.</p>
<p>When upgrading to Turbo 8, keep <code>scroll: reset</code> as the default and make sure you manually test the pages where <code>scroll: preserve</code> is set to ensure it is behaving the way you intended.</p>
<h2 id='2-the-autofocus-attribute-can-unexpectedly-cause-the-page-to-quot-jump-quot-when-a-morph-is-applied' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#2-the-autofocus-attribute-can-unexpectedly-cause-the-page-to-quot-jump-quot-when-a-morph-is-applied' aria-label='Anchor'></a>2. The <code>autofocus</code> attribute can unexpectedly cause the page to “jump” when a morph is applied</h2>
<p>I ran into an issue where a page would inexplicably jump to the top of the viewport when I set Turbo 8 to <code>preserve: scroll</code>. Turns out I had a form field with a <code><form autofocus></code> attribute for a search input in the user interface. When Turbo 8 diffed the DOM and refreshed, it would scroll to the <code>autofocus</code> form element no matter where I was on the page.</p>
<p>Turbo 8 uses the <a href='https://github.com/bigskysoftware/idiomorph' title=''>ideomorph</a> library to perform client-side DOM diffing operations. <a href='https://github.com/bigskysoftware/idiomorph#callbacks' title=''>Callbacks exist</a> that you could attach to with the browser debugger to troubleshoot these issues, but it’s not as straight forward as it ideally would be for a great debugging experience.</p>
<h2 id='3-add-a-data-turbo-permanent-attribute-to-form-inputs-and-other-html-elements-that-you-want-to-preserve-between-refreshes' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#3-add-a-data-turbo-permanent-attribute-to-form-inputs-and-other-html-elements-that-you-want-to-preserve-between-refreshes' aria-label='Anchor'></a>3. Add a <code>data-turbo-permanent</code> attribute to form inputs and other HTML elements that you want to preserve between refreshes</h2>
<p>At some point you’ll have a Turbo 8 page morph wipe out a form when the data gets updated. This means users editing a form could lose their work. The solution? The <code>data-turbo-permanent</code> attribute can be added to form inputs or HTML elements that shouldn’t get updated by the server.</p>
<p>Careful though; when the user saves this data to the server, it could overwrite the newer data on the server.</p>
<h2 id='4-paying-attention-to-caching-is-even-more-crucial-for-performance' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#4-paying-attention-to-caching-is-even-more-crucial-for-performance' aria-label='Anchor'></a>4. Paying attention to caching is even more crucial for performance</h2>
<p>Since Turbo 8 morphing is conceptually a glorified page reloader, it’s important to pay attention to the performance of HTML page renditions. The good news is that there are ample solutions to this problem because developers have been solving it since HTTP arrived on scene with caching for decades.</p>
<p>Yes, triggering the pull of an HTML document by the client <em>will</em> use more bandwidth than carefully calculated server-side diffs that are sent over the wire, but if the payloads are compressed via gzip or Brotli and the HTML document sizes aren’t insanely large, it is a small price to pay to not have to <a href='https://dev.37signals.com/exploring-server-side-diffing-in-turbo/' title=''>deal with the complexity of server-side diffing</a> that the developers of Turbo 8 tried and abandoned.</p>
<p>At first glance, Turbo 8 page morphing seems like a sledge hammer approach to building low latency UI, but when you consider how caching is built into browsers, proxies, and frameworks—it’s really an elegant and balanced way to solve the problem.</p>
<h2 id='5-turbo-rails-meta-tags-dont-appear-where-theyre-called-in-the-views' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#5-turbo-rails-meta-tags-dont-appear-where-theyre-called-in-the-views' aria-label='Anchor'></a>5. Turbo Rails meta tags don’t appear where they’re called in the views</h2>
<p>The current Turbo helpers are implemented in a manner where meta tags are emitted into <code>content_for :head</code>, which is not where you may be calling the tags from view files. If you forget to include the <code>content_for :head</code> block your layouts, the Turbo meta tags won’t show up and you’ll wonder why your settings aren’t being picked up.</p>
<p>For example, calling <code>turbo_refreshes_with method: :morph, scroll: :preserve</code> will append a meta tag in the <code>content_for :head</code> block. If your application calls it twice, you’ll have two of these tags appear in the <code>:head</code> block.</p>
<p>When you first setup Turbo 8, take a few minutes to verify manually or with a test that the turbo meta tags do indeed appear in the <code></head></code> tags.</p>
<p>If you need meta tags to appear where you call them, all the turbo helpers since Beta 2 have a <code>_tag</code> method that will emit the tag where it’s called in the view. For example, <code>turbo_refreshes_with</code> would be appended to <code>content_for :head</code> and <code>turbo_refreshes_with_tag</code> will appear where it’s called.</p>
<p>The Rails Core team has <a href='https://github.com/hotwired/turbo-rails/pull/542' title=''>decided to stick with this approach</a> since changing it would break Rails apps being upgraded from Turbo 7 or older, but it is a level of indirection and inconsistency that’s helpful to understand and manually test.</p>
<h2 id='6-slow-down-the-development-environment-to-see-quot-loading-quot-states' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#6-slow-down-the-development-environment-to-see-quot-loading-quot-states' aria-label='Anchor'></a>6. Slow down the development environment to see “loading” states</h2>
<p>When you start building applications in a development environment it’s a good idea to slow things down so you can get a better feel for how it looks and feels while running under adverse, slow, and congested conditions.</p>
<p>I created the “Simulated Slowness” concern for my local development environment so I could experience all of the loading UI that my users might see if my server is under heavy load or the user is using the application from a universe far far away.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-c697vqbg"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-c697vqbg"><span class="c1"># ./app/models/concerns/simulated_slowness.rb</span>
<span class="k">module</span> <span class="nn">SimulatedSlowness</span>
<span class="c1"># Simulates a delay in a development environment so we don't get spoiled</span>
<span class="c1"># by everything being super fast all the time.</span>
<span class="k">def</span> <span class="nf">simulate_delay</span><span class="p">(</span><span class="n">seconds</span> <span class="o">=</span> <span class="mi">5</span><span class="p">)</span>
<span class="k">if</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">env</span><span class="p">.</span><span class="nf">development?</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">logger</span><span class="p">.</span><span class="nf">debug</span> <span class="s2">"Sleeping for </span><span class="si">#{</span><span class="n">seconds</span><span class="si">}</span><span class="s2"> seconds 🥱"</span>
<span class="n">seconds</span><span class="p">.</span><span class="nf">times</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">n</span><span class="o">|</span>
<span class="nb">sleep</span> <span class="mi">1</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">logger</span><span class="p">.</span><span class="nf">debug</span> <span class="s2">"Sleeping for </span><span class="si">#{</span><span class="n">n</span><span class="si">}</span><span class="s2"> seconds 😴"</span>
<span class="k">end</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">logger</span><span class="p">.</span><span class="nf">debug</span> <span class="s2">"Awake after </span><span class="si">#{</span><span class="n">seconds</span><span class="si">}</span><span class="s2"> seconds 😀"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>I include this in my code, in this example the <code>ApplicationModel</code>.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-44e8pqun"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-44e8pqun"><span class="k">class</span> <span class="nc">ApplicationModel</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="kp">include</span> <span class="no">SimulatedSlowness</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Then from where you need to simulate production taking a while:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-un1l7aum"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-un1l7aum"><span class="k">def</span> <span class="nf">perform</span>
<span class="n">simulate_delay</span> <span class="mi">4</span><span class="p">.</span><span class="nf">seconds</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>In the development logs you’ll see this:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-xr8i4zfm"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-xr8i4zfm">web | [ActiveJob] ... Sleeping for 5 seconds 🥱
web | [ActiveJob] ... Sleeping for 0 seconds 😴
web | [ActiveJob] ... Sleeping for 1 seconds 😴
web | [ActiveJob] ... Sleeping for 2 seconds 😴
web | [ActiveJob] ... Sleeping for 3 seconds 😴
web | [ActiveJob] ... Sleeping for 4 seconds 😴
web | [ActiveJob] ... Awake after 5 seconds 😀
</code></pre>
</div>
</div>
<p>Now you’ll get a better feel for what your Turbo 8 UI should look like in between states when a worker queue, model, or something else, is running slower than usual.</p>
<p>This will help you take loading states into account, which is particularly important for long running background jobs.</p>
<h2 id='7-the-turbo-cable-stream-source-tag-can-break-grid-and-flex-layouts' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#7-the-turbo-cable-stream-source-tag-can-break-grid-and-flex-layouts' aria-label='Anchor'></a>7. The <code>turbo-cable-stream-source</code> tag can break grid and flex layouts</h2>
<p>When subscribing to Turbo Stream channels to receive page morph notifications, the <code>turbo-cable-stream-source</code> tag is emitted directly to where its called in the view. If this tag is emitted into a CSS grid, it might be included in the layout and create gaps that you’ll find surprising.</p>
<p>You can either set the tag to <code>display: none</code> or emit the tag in a place that won’t break CSS grid or flex layouts, like this:</p>
<div class="highlight-wrapper group relative xml">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-v6r154ka"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-v6r154ka"><span class="nt"><turbo-cable-stream-source</span> <span class="na">channel=</span><span class="s">"Turbo::StreamsChannel"</span> <span class="na">signed-stream-name=</span><span class="s">"IloybGtPaTh2YzJWeWRtVnlMMVZ6WlhJdk1RIg==--b4bcfff51ae4074540fdefbada55a237d68206bf960bd30a6684b310a255656c"</span> <span class="na">class=</span><span class="s">"hidden"</span> <span class="na">style=</span><span class="s">"display: none;"</span> <span class="na">connected=</span><span class="s">""</span><span class="nt">></turbo-cable-stream-source></span>
<span class="c"><!-- Keep the cable tag out of the grid flow --></span>
<span class="nt"><div</span> <span class="na">id=</span><span class="s">"post_1"</span> <span class="na">class=</span><span class="s">"grid grid-columns-2"</span><span class="nt">></span>
<span class="c"><!-- ... --></span>
<span class="nt"></div></span>
</code></pre>
</div>
</div><h2 id='8-lazy-loading-content-with-turbo_frames-still-has-its-place' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#8-lazy-loading-content-with-turbo_frames-still-has-its-place' aria-label='Anchor'></a>8. Lazy loading content with <code>turbo_frames</code> still has its place</h2>
<p>It’s not really a “gotcha”, but turbo frames still have their place for lazy loading content. For example, if you have a long list of content that you don’t want to load all at once you’d paginate it with a lazily loading <code>turbo_frame</code> and a pagination library.</p>
<p>They are also still useful for highly specialized and localized operations, like an autocomplete UI interaction when typing into a form input.</p>
<h2 id='wrap-up' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#wrap-up' aria-label='Anchor'></a>Wrap up</h2>
<p>Turbo 8 is huge improvement over Turbo 7, but like any software there’s a few quirks about it that are helpful to keep in mind to make the upgrade and development process a bit smoother.</p>
<p>As always, there’s a lot of room for improvement for the developer experience including better client-side debugging tools, a client-side API to handle conflict resolution for DOM merging elements like form inputs, documentation, and helper methods. All of these are great opportunities for community contributions to the <a href='https://hotwired.dev' title=''>Hotwire project suite</a>.</p>
<p>Overall, Turbo 8 continues the tradition of making Rails even more productive with its HTML-over-the-wire DOM diffing abilities and provide ample opportunities to remove code from most Rails applications.</p>
/ruby-dispatch/8-turbo-8-gotchas/assets/cover.webpTurbo 8 in 8 minuteshttps://fly.io/ruby-dispatch/turbo-8-in-8-minutes/2023-11-29T00:00:00+00:002023-12-13T15:30:20+00:00<p><strong class='font-[550] text-navy-950'>Turbo 8 simplifies the development of live-updating Rails applications. It offers a dramatic leap forward from previous versions by minimizing the need for manually coding turbo frames and turbo stream responses. This advancement simplifies both the creation and maintenance of Rails applications making you even more productive.</strong></p>
<p>If you’re not familiar with <a href='https://turbo.hotwired.dev' title=''>Turbo</a>, it’s a library used widely by Ruby on Rails applications to partially update pages, making them feel as responsive as single-page JavaScript applications. It’s similar to frameworks like <a href='https://htmx.org' title=''>HTMX</a>, <a href='https://docs.stimulusreflex.com' title=''>StimulusReflex</a>, <a href='https://github.com/phoenixframework/phoenix_live_view' title=''>Phoenix LiveView</a>, and <a href='https://laravel-livewire.com' title=''>Laravel LiveWire</a>.</p>
<h2 id='think-of-turbo-8-as-a-really-smart-page-reloader' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#think-of-turbo-8-as-a-really-smart-page-reloader' aria-label='Anchor'></a>Think of Turbo 8 as a really smart page reloader</h2>
<p>That’s an oversimplification, but the analogy is helpful to better understand how it works. The idea is this:</p>
<ol>
<li><strong class='font-[550] text-navy-950'>Rails publishes when data changes</strong> - Rails models with <code>broadcasts_refreshes</code> will publish when a model has been created, updated, or destroyed via ActionCable.
</li><li><strong class='font-[550] text-navy-950'>Pages subscribe to data changes they care about</strong> - When a page is loaded, the Turbo JavaScript scans it looking for <code><turbo-cable-stream-source/></code> tags. Each tag describes the model class and ID that is used to subscribe to data change notifications over ActionCable.
</li><li><strong class='font-[550] text-navy-950'>When data changes on a page, the changes are applied to the page</strong> - When a model is updated, the subscribed pages receive notifications that something has changed. Turbo then requests <em>the entire HTML page</em> in the background via HTTP and compares the new HTML to the old HTML that’s currently loaded. If there are differences between the HTML files, it will apply <em>only</em> the differences to the page without reloading the entire page.
</li></ol>
<p>That’s it. That’s the framework. It’s impressive how unimpressive it is. Most tutorials I’ve seen to date get caught up in comparing Turbo 7 with Turbo 8, but I think that makes it harder to understand, so forget everything you know about older versions of Turbo and let’s have a look at how things will work in the future by trying the beta.</p>
<h2 id='install-the-turbo-8-beta' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#install-the-turbo-8-beta' aria-label='Anchor'></a>Install the Turbo 8 beta</h2>
<p>Install the Turbo 8 gem by adding this to your Gemfile or updating the existing <code>turbo-rails</code> entry.</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-eu8zogtj"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-eu8zogtj"><span class="c"># Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]</span>
gem <span class="s2">"turbo-rails"</span>, <span class="s2">"~> 2.0.0.pre.beta"</span>
</code></pre>
</div>
</div>
<p>Restart your Rails server and get ready to go!</p>
<h2 id='add-the-turbo-tag-to-the-head-of-the-page' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#add-the-turbo-tag-to-the-head-of-the-page' aria-label='Anchor'></a>Add the Turbo tag to the head of the page</h2>
<p>First thing you’ll need to do is add the tag in the <code><head/></code> tag of your application layout.</p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-pf4imwqv"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-pf4imwqv"><span class="cp"><%=</span> <span class="n">turbo_refreshes_with</span> <span class="ss">method: :morph</span><span class="p">,</span> <span class="ss">scroll: :preserve</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">content_for</span> <span class="ss">:head</span> <span class="cp">%></span>
</code></pre>
</div>
</div>
<p>This configures the behavior of Turbo 8 to “morph” the page and preserves the scroll position. The “old” behavior of Turbo would “replace” the entire page and “reset” the scroll position.</p>
<p>You’ll still need the “traditional” behavior of Turbo to load content pages. If you apply <code>method: :morph, scroll: preserve</code> to everything, like content pages, you’ll get strange behaviors where the user clicks on the content and the page starts from the middle instead of the top.</p>
<h2 id='pages-subscribe-to-models-to-stay-informed-about-changes-and-reload-accordingly' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#pages-subscribe-to-models-to-stay-informed-about-changes-and-reload-accordingly' aria-label='Anchor'></a>Pages subscribe to models to stay informed about changes and reload accordingly</h2>
<p>When you want a view to be updated, you’ll subscribe to it from your application view files. For example, if you have a blog post that you want to update when the author publishes a change, you’d add this to the top of <code>./app/views/post/show.html.erb</code></p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-5r0xnaen"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-5r0xnaen"><span class="cp"><%=</span> <span class="n">turbo_stream_from</span> <span class="vi">@post</span> <span class="cp">%></span>
<span class="nt"><h1></span><span class="cp"><%=</span> <span class="vi">@post</span><span class="p">.</span><span class="nf">title</span> <span class="cp">%></span><span class="nt"></h1></span>
</code></pre>
</div>
</div>
<p>This helper emits a tag like this in the HTML:</p>
<div class="highlight-wrapper group relative html">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-my5lw2dw"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-my5lw2dw"><span class="nt"><turbo-cable-stream-source</span> <span class="na">channel=</span><span class="s">"Turbo::StreamsChannel"</span> <span class="na">signed-stream-name=</span><span class="s">"IloybGtPaTh2YzJWeWRtVnlMMVZ6WlhJdk1RIg==--b4bcfff51ae4074540fdefbada55a237d68206bf960bd30a6684b310a255656c"</span> <span class="na">connected=</span><span class="s">""</span><span class="nt">></turbo-cable-stream-source></span>
</code></pre>
</div>
</div>
<p>It might look cryptic, but within the <code>signed-stream-name</code> attribute is in the model class and ID Turbo subscribes to for changes. When the post is updated, Turbo gets a signal from the server, “this blog post changed”. It then makes an HTTP request for the current HTML page via JavaScript, diffs the new HTML to the DOM that’s already loaded, and merges the changes between the two so that it doesn’t have to reload the page.</p>
<p>Great! Now we have a client that’s subscribing to our server for changes, but that’s only half of it. We need to tell Rails to publish changes to the <code>Post</code> model, so we add the following to the <code>Post</code> model.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-q4y2ud7n"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-q4y2ud7n"><span class="k">class</span> <span class="nc">Post</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="c1"># When the model instance is changed, a message will sent over</span>
<span class="c1"># ActionCable that notifies the page to reload.</span>
<span class="n">broadcasts_refreshes</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Now when we create, update, or destroy a <code>Post</code> model, Rails will publish it over ActionCable and notify all the interested pages to reload if a change is made.</p>
<h2 id='how-to-update-collections' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#how-to-update-collections' aria-label='Anchor'></a>How to Update Collections</h2>
<p>Collections all usually belong to something in an application. For example, a blog has many posts. We’d probably have a view somewhere that lists all of the posts for a blog at <code>./app/views/blog/posts/index.html.erb</code></p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-yuicfaz0"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-yuicfaz0"><span class="cp"><%=</span> <span class="n">turbo_stream_from</span> <span class="vi">@blog</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="vi">@blog</span><span class="p">.</span><span class="nf">posts</span> <span class="cp">%></span>
</code></pre>
</div>
</div>
<p>Then we add to our <code>Post</code> model the association that it belongs to a <code>Blog</code>. The important thing is that we add <code>touch: true</code>.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-mun1s9ut"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-mun1s9ut"><span class="k">class</span> <span class="nc">Post</span>
<span class="c1"># Touch will update the timestamp on the blog when</span>
<span class="c1"># a post is created, updated, or destroyed.</span>
<span class="n">belongs_to</span> <span class="ss">:blog</span><span class="p">,</span> <span class="ss">touch: </span><span class="kp">true</span>
<span class="c1"># When the model is changed, a message will sent over ActionCable.</span>
<span class="n">broadcasts_refreshes</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Then the <code>Blog</code> model needs to broadcast its refreshes:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-s7p52ye8"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-s7p52ye8"><span class="k">class</span> <span class="nc">Blog</span>
<span class="n">has_many</span> <span class="ss">:posts</span>
<span class="n">broadcasts_refreshes</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Now when a post is created, updated, or deleted, the blog model will get its timestamp updated and trigger a refresh on the pages listening for changes to the blog instance.</p>
<h2 id='what-about-collections-that-dont-belong-to-anything' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#what-about-collections-that-dont-belong-to-anything' aria-label='Anchor'></a>What about collections that don’t belong to anything?</h2>
<p>In practice this is rare in an application. For example, a blog probably belongs to a user or an account, which could be “touched” similarly to above. Here’s what that might look like.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-sfb1vjsj"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-sfb1vjsj"><span class="k">class</span> <span class="nc">Blog</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="c1"># Code from above removed for clarity</span>
<span class="n">belongs_to</span> <span class="ss">:user</span><span class="p">,</span> <span class="ss">touch: </span><span class="kp">true</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Then add <code>broadcast_refreshes</code> to the <code>User</code> model.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-uic9z142"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-uic9z142"><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">has_many</span> <span class="ss">:blogs</span>
<span class="c1"># Blog will touch the account when something is changed.</span>
<span class="n">broadcasts_refreshes</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>And on the dashboard page that lists all of the users’ blogs, listen for changes to accounts:</p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-im1805gj"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-im1805gj"><span class="cp"><%=</span> <span class="n">turbo_stream_from</span> <span class="n">current_user</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="vi">@current_user</span><span class="p">.</span><span class="nf">blogs</span> <span class="cp">%></span>
</code></pre>
</div>
</div>
<p>If your application has a collection that truly can’t be modeled with a parent object, you still have access to turbo streams to append to a list.</p>
<h2 id='use-postgres-or-sqlite-to-broadcast-changes' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#use-postgres-or-sqlite-to-broadcast-changes' aria-label='Anchor'></a>Use Postgres or SQLite to broadcast changes</h2>
<p>Since Turbo 8 doesn’t have to push HTML payloads over WebSockets, the 8000 byte limit of the Postgres ActionCable adapter is no longer an issue. If you’re only using Redis or ActionCable pub-sub on a small to medium size application, this can simplify the infrastructure of your application by eliminating Redis as a dependency.</p>
<p>If you’re deploying a SQLite application to production, you can install <a href='https://github.com/oldmoe/litestack' title=''>Litestack</a> in your Rails application and use <a href='https://github.com/oldmoe/litestack#actioncable' title=''>Litecable</a> to publish change notifications over Turbo 8.</p>
<h2 id='get-excited-again-for-the-future-of-rails-development' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#get-excited-again-for-the-future-of-rails-development' aria-label='Anchor'></a>Get excited again for the future of Rails development</h2>
<p>The tiny amount of effort required to make Rails applications auto-update is astonishing. Turbo 8 is still in beta, and there are lots of edge cases to consider for this approach to building Rails applications, but the technology is shaping up to be a promising way to further simplify Rails application development.</p>
<p>If you’re heavily invested in Turbo Frames in versions prior to Turbo 8, the hardest part of moving over will probably be removing all the <code>format.turbo_stream</code> blocks in your controller code and <code>turbo_frame</code> tags from your views. If you believe git commits with thousands of deleted lines of code is productive, be prepared for some very productive days of development ahead.</p>
/ruby-dispatch/turbo-8-in-8-minutes/assets/turbo-8-cover.webpSQLite Persistence Canaryhttps://fly.io/ruby-dispatch/sqlite-persistence-canary/2023-11-01T00:00:00+00:002023-11-21T21:08:38+00:00<p>As the world boldly moves towards running SQLite in production, there’s a bit of a problem that looms in today’s container-based production environments: persistence.</p>
<p>Ever since cloud servers arrived on scene, we’ve been told over and over again to not depend on writing important data to disk because disks fail. Docker took this a step further by making it common practice to rebuild the entire file system from scratch on every deploy. There’s lots to like about this approach, and generally we’re much better off for it, but it does create a huge problem for SQLite in production: how do you properly educate developers their rebuild-the-world-from-scratch deployments could destroy their production data?</p>
<h2 id='a-warning-message-isnt-enough' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#a-warning-message-isnt-enough' aria-label='Anchor'></a>A warning message isn’t enough</h2>
<p><a href='https://github.com/rails/rails/pull/42191' title=''>In 2021 Rails introduced a way of displaying a warning message</a> in the logs:</p>
<blockquote>
<p>You are running SQLite in production, this is generally not recommended. You can disable this warning by setting <code>config.active_record.sqlite3_production_warning=false</code></p>
</blockquote>
<p>Cool, but let’s be real—it’s really easy to miss log file messages, especially if you’re new to web applications and deploying them to production. It’s highly likely this log message would be seen <em>after</em> data is lost in production.</p>
<p>This was actually a very reasonable approach in 2021 before the industry seriously considered running SQLite workloads in production, but as Bob Dylan famously said, “the times they are a-changin’”.</p>
<h2 id='test-persistence-between-the-first-and-second-deploys' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#test-persistence-between-the-first-and-second-deploys' aria-label='Anchor'></a>Test persistence between the first and second deploys</h2>
<p>What if instead we could test the persistence of the application within the environment?</p>
<p>Here’s how this might work:</p>
<h3 id='first-deployment' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#first-deployment' aria-label='Anchor'></a>First deployment</h3>
<ol>
<li>When Rails boots, there’s no SQLite database on the file system so it creates one on disk and refuses to boot with an error message: “Rails detected SQLite in production. Re-deploy your application to test if your file system will save your production data between deploys”
</li><li>A file would also be written to <code>./tmp/persistence-test.lock</code> that would prevent Rails from booting if a SQLite database and this file are present. This deals with environments that try to boot applications multiple times when they fail to boot.
</li></ol>
<p>The key friction point this introduces would force developers to deploy their application again, which is where the file system can actually be tested to persist data between deploys.</p>
<h3 id='second-deployment' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#second-deployment' aria-label='Anchor'></a>Second deployment</h3>
<p>Assuming persistence is properly setup on the production environment, Rails would perform the following checks:</p>
<ol>
<li>When Rails boots, it checks for the existence of the SQLite database in production.
</li><li>Since the SQLite database exists in a persistent volume, Rails then checks to see if the <code>./tmp/persistence-test.lock</code> file is present. Assuming the <code>./tmp</code> directory is wiped out between deploys (it should), Rails will boot as you’d expect and be reasonably confident that the SQLite database is being stored on a persistent disk.
</li></ol>
<p>This approach introduces the “right” friction points for developers who want to deploy their SQLite applications to production, minimizes surprises, and teaches them along the way about an abstraction they need to consider in their production environment.</p>
<h2 id='additional-considerations' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#additional-considerations' aria-label='Anchor'></a>Additional considerations</h2>
<p>There’s a lot to think about with this approach:</p>
<h3 id='edge-cases-should-be-fail-safe' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#edge-cases-should-be-fail-safe' aria-label='Anchor'></a>Edge cases should be fail-safe</h3>
<p>It’s possible that the <code>./tmp</code> file is <em>not</em> erased between deploys. In that case Rails could display within the error message instruction that the developer can delete that file if they’re confident their database is being written to a persistent disk.</p>
<p>This is actually a good trade-off because it errs on the side of safety and runs developers through all the things they should think about with respect to writing data to a file on disk.</p>
<h3 id='is-crashing-the-first-deploy-a-good-idea' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#is-crashing-the-first-deploy-a-good-idea' aria-label='Anchor'></a>Is crashing the first deploy a good idea?</h3>
<p>It does seem a bit crazy to intentionally crash the first production deployment, but it’s even crazier to write production data to a location that could potentially be erased on the next deploy.</p>
<p>The other thing to keep in mind about a first deployment is that it doesn’t have users yet, so there’s little to worry about in terms of downtime.</p>
<h3 id='its-scary-having-a-mechanism-that-could-intentionally-crash-rails-if-i-dont-want-it-to' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#its-scary-having-a-mechanism-that-could-intentionally-crash-rails-if-i-dont-want-it-to' aria-label='Anchor'></a>Its scary having a mechanism that could intentionally crash Rails if I don’t want it to</h3>
<p>There is a legitimate concern for the remote possibility that a <code>./tmp/persistence-test.lock</code> file makes it somehow into a production environment.</p>
<p>In this case, the <code>config.active_record.sqlite3_production_warning=false</code> could completely disable the persistence test and restore confidence that Rails would never enter this state when booting.</p>
<h2 id='wrap-up' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#wrap-up' aria-label='Anchor'></a>Wrap-up</h2>
<p><a href='https://fly.io/ruby-dispatch/sqlite-and-rails-in-production/' title=''>The prospect of running SQLite & Rails in production</a> is exciting! It stands to greatly simplify the infrastructure needed to deploy small-to-medium size production applications be eliminating the need for running Postgres and Redis services, especially when used with libraries like <a href='https://github.com/oldmoe/litestack' title=''>Litestack</a>, but extra thought and care must be put into giving developers the information they need ensure they don’t lose production data.</p>
<p>This method is applicable not just to Rails, but other frameworks as well that face similar problems of potentially losing production data written to an ephemeral disk.</p>
/ruby-dispatch/sqlite-persistence-canary/assets/sqlite-persistence-canary.webpAudience of Onehttps://fly.io/ruby-dispatch/audience-of-one/2023-10-26T00:00:00+00:002024-02-05T16:04:11+00:00<div class="lead"><p>We’re Fly.io. We run apps for our users on hardware we host around the world. Fly.io happens to be a great place to run Rails applications. Check out how to <a href="/docs/rails/" title="">get started</a>!</p>
</div>
<p>I’ve got an <a href='https://smooth.fly.dev/' title=''>app for scheduling dance competitions</a>.
I <a href='http://intertwingly.net/blog/2022/08/13/Unretiring' title=''>unretired</a> to explore hosting it on the cloud. I’ve <a href='https://fly.io/ruby-dispatch/smooth-fly-dev/' title=''>written about</a> how it is configured.</p>
<p>In the first year this application was used to schedule events in six cities. As we are coming to a close of the second year the current count in twenty six cities. I need to be prepared in case the number of cities quadruples again next year.</p>
<p>Deploying a new event, location, or even region is merely a matter of updating a few configuration files and then running a script. This typically takes only a few minute, but do it enough times and those minutes add up. My goal is to automate what I’m doing manually so it can be done in seconds.</p>
<p>I’ve started by creating forms and having the controller update text files; this effort is fairly mundane and routine. Launching a script asynchronously from a browser process and streaming the response back live to the browser as it runs is less common. Mark Ericksen recently wrote an <a href='https://fly.io/phoenix-files/star-cross-live-view-processes/' title=''>blog article</a> on how to do similar things with Phoenix. The building blocks available for Rails are quite different, so a different approach is needed.</p>
<p>This blog post will show you how to build a Rails application that streams <a href='https://fly.io/docs/flyctl/logs/' title=''><code>fly logs</code></a> output to the browser. You can already find this functionality in the dashboard, but the point is that if you can stream the output of that command you can handle any command.</p>
<p>Let’s get started. If you are impatient, ignore the text and with a few copy and pastes you can have a demo up and running. After that skip down to the <a href='#summary' title=''>summary</a>.</p>
<h2 id='step-1-generate-a-new-application' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#step-1-generate-a-new-application' aria-label='Anchor'></a>Step 1: Generate a new application</h2><div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-cy6nj3vw"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-cy6nj3vw">rails new console --css=tailwind
cd console
bin/rails generate channel output
bin/rails generate stimulus submit
bin/rails generate controller demo cmd
bundle add ansi-to-html
</code></pre>
</div>
</div>
<p>We will be using <a href='https://tailwindcss.com/' title=''>Tailwindcss</a> for styling,
<a href='https://guides.rubyonrails.org/action_cable_overview.html' title=''>Action Cable</a> to stream the results,
<a href='https://stimulus.hotwired.dev/' title=''>Stimulus</a> to wire the UI to the cable,
and a vanilla <a href='https://guides.rubyonrails.org/action_controller_overview.html' title=''>controller</a> for the server side logic.</p>
<p>Because the output of <code>fly logs</code> is colorized with <a href='https://en.wikipedia.org/wiki/ANSI_escape_code' title=''>ANSI escape codes</a>, we will also use <a href='https://rubygems.org/gems/ansi-to-html/' title=''>ansi-to-html</a> to convert these colors to HTML.</p>
<p>Before we proceed any further, comment out the channel in <code>app/javascript/channels/index.js</code> as we won’t be using it directly:</p>
<div class="highlight-wrapper group relative javascript">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-fnv93jlz"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-fnv93jlz"><span class="c1">// Import all the channels to be used by Action Cable</span>
<span class="c1">// import "channels/output_channel"</span>
</code></pre>
</div>
</div>
<p>The reason we are not using the (singular) output channel directly is that Action Cable is designed to broadcast data to all subscribers. This won’t do at all for this use case. Instead we will be creating separate streams for each client and then direct each output to a specific stream, thereby effectively broadcasting the results of our commands to an audience of one.</p>
<h2 id='step-2-create-an-html-form' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#step-2-create-an-html-form' aria-label='Anchor'></a>Step 2: Create an HTML form</h2>
<p>Next replace <code>app/views/demo/command.html.erb</code> with the following:</p>
<div class="highlight-wrapper group relative html">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-16ejv1au"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-16ejv1au"><span class="nt"><div</span> <span class="na">class=</span><span class="s">"w-full"</span> <span class="na">data-controller=</span><span class="s">"submit"</span><span class="nt">></span>
<span class="nt"><h1</span> <span class="na">class=</span><span class="s">"text-4xl font-extrabold text-center"</span><span class="nt">></span>Command demo<span class="nt"></h1></span>
<span class="nt"><input</span> <span class="na">data-submit-target=</span><span class="s">"input"</span> <span class="na">name=</span><span class="s">"app"</span> <span class="na">placeholder=</span><span class="s">"appname"</span>
<span class="na">class=</span><span class="s">"appearance-none mt-4 w-40 mx-auto block bg-gray-200
text-gray-700 border rounded py-3 px-4 mb-3
leading-tight focus:outline-none focus:bg-white"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">data-submit-target=</span><span class="s">"submit"</span>
<span class="na">class=</span><span class="s">"flex mx-auto bg-blue-500 hover:bg-blue-700 text-white
font-bold py-2 px-4 border-2 rounded-xl
my-4"</span><span class="nt">></span>submit<span class="nt"></button></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"border-2 border-black rounded-xl p-2 hidden"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">data-submit-target=</span><span class="s">"output"</span> <span class="na">data-stream=</span><span class="s">""</span>
<span class="na">class=</span><span class="s">"w-full mx-auto overflow-y-auto h-auto font-mono text-sm
max-h-[25rem] min-h-[25rem]"</span><span class="nt">></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
</code></pre>
</div>
</div>
<p>This is standard HTML. It doesn’t even use any Rails form helpers.
Nor even a HTML <code><form></code> element - the fields will be wired together
using Stimulus. Notes:</p>
<ul>
<li><code>data-controller</code> names the stimulus class (<code>submit</code>)
</li><li><code>data-submit-target</code> identified the <code>input</code> field(s),
the <code>submit</code> button, and the <code>output</code> area.
</li><li><code>data-steam</code> on the output target contains the one bit of ERB,
and that contain the token that will uniquely identify the stream.
</li></ul>
<p>The tailwind CSS stylings are taken from the Monitoring tab from the
fly io dashboard, minus the background color.</p>
<p>The <code>div</code> element that contains the output is initially hidden.</p>
<h2 id='part-3-wire-the-form-elements-to-the-channel' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#part-3-wire-the-form-elements-to-the-channel' aria-label='Anchor'></a>Part 3: wire the form elements to the channel</h2>
<p>Replace <code>app/javascript/controllers/submit_controller.js</code> with the
following:</p>
<div class="highlight-wrapper group relative javascript">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-faaa14v0"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-faaa14v0"><span class="k">import</span> <span class="p">{</span> <span class="nx">Controller</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@hotwired/stimulus</span><span class="dl">"</span>
<span class="k">import</span> <span class="nx">consumer</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../channels/consumer</span><span class="dl">'</span>
<span class="c1">// Connects to data-controller="submit"</span>
<span class="k">export</span> <span class="k">default</span> <span class="kd">class</span> <span class="kd">extends</span> <span class="nx">Controller</span> <span class="p">{</span>
<span class="kd">static</span> <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span> <span class="dl">"</span><span class="s2">input</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">submit</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">output</span><span class="dl">"</span> <span class="p">]</span>
<span class="nx">connect</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">buttonTarget</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">click</span><span class="dl">'</span><span class="p">,</span> <span class="nx">event</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">event</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">()</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">outputTarget</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span>
<span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">input</span> <span class="k">of</span> <span class="k">this</span><span class="p">.</span><span class="nx">inputTargets</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">params</span><span class="p">[</span><span class="nx">input</span><span class="p">.</span><span class="nx">name</span><span class="p">]</span> <span class="o">=</span> <span class="nx">input</span><span class="p">.</span><span class="nx">value</span>
<span class="p">}</span>
<span class="nx">consumer</span><span class="p">.</span><span class="nx">subscriptions</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span>
<span class="na">channel</span><span class="p">:</span> <span class="dl">"</span><span class="s2">OutputChannel</span><span class="dl">"</span><span class="p">,</span>
<span class="na">stream</span><span class="p">:</span> <span class="nx">outputTarget</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">stream</span>
<span class="p">},</span> <span class="p">{</span>
<span class="nx">connected</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">perform</span><span class="p">(</span><span class="dl">"</span><span class="s2">command</span><span class="dl">"</span><span class="p">,</span> <span class="nx">params</span><span class="p">)</span>
<span class="nx">outputTarget</span><span class="p">.</span><span class="nx">parentNode</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="dl">"</span><span class="s2">hidden</span><span class="dl">"</span><span class="p">)</span>
<span class="p">},</span>
<span class="nx">received</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">div</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">"</span><span class="s2">div</span><span class="dl">"</span><span class="p">)</span>
<span class="nx">div</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">"</span><span class="s2">class</span><span class="dl">"</span><span class="p">,</span>
<span class="dl">"</span><span class="s2">pb-2 break-all overflow-x-hidden</span><span class="dl">"</span><span class="p">)</span>
<span class="nx">div</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="nx">data</span>
<span class="kd">let</span> <span class="nx">bottom</span> <span class="o">=</span> <span class="nx">outputTarget</span><span class="p">.</span><span class="nx">scrollHeight</span> <span class="o">-</span>
<span class="nx">outputTarget</span><span class="p">.</span><span class="nx">scrollTop</span> <span class="o">-</span>
<span class="nx">outputTarget</span><span class="p">.</span><span class="nx">clientHeight</span>
<span class="nx">outputTarget</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">div</span><span class="p">)</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">bottom</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="nx">div</span><span class="p">.</span><span class="nx">scrollIntoView</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="p">})</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
</div>
</div>
<p>This stimulus class:</p>
<ul>
<li>Identifies the three “targets” to match in the HTML: <code>input</code>, <code>submit</code>,
and <code>output</code>.
</li><li>Defines an action to be executed when the submit button is clicked
</li><li>Extracts the outputTarget and the name and value of each of the inputs.
</li><li>Creates a subscription on the <code>OutputChannel</code>, identifying the substream
taken from the output target element. Two functions are defined
for the subscription:
<ul>
<li><code>connected</code> will request that the command be performed, passing
the parameters constructed from the input(s). Additionally the
enclosing element for the output target will be unhidden.
</li><li><code>received</code> will add a line to the output. If the output stream
was scrolled to the bottom at the time a line is added the
scroll will advance to show the new content.
</li></ul>
</li></ul>
<h2 id='part-4-wire-the-form-elements-to-the-channel' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#part-4-wire-the-form-elements-to-the-channel' aria-label='Anchor'></a>Part 4: wire the form elements to the channel</h2>
<p>Replace <code>app/controllers/demo_controller.rb</code> with the following:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-owayyxjn"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-owayyxjn"><span class="k">class</span> <span class="nc">DemoController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="k">def</span> <span class="nf">cmd</span>
<span class="vi">@stream</span> <span class="o">=</span> <span class="no">OutputChannel</span><span class="p">.</span><span class="nf">register</span> <span class="k">do</span> <span class="o">|</span><span class="n">params</span><span class="o">|</span>
<span class="p">[</span><span class="s2">"flyctl"</span><span class="p">,</span> <span class="s2">"logs"</span><span class="p">,</span> <span class="s2">"--app"</span><span class="p">,</span> <span class="n">params</span><span class="p">[</span><span class="s2">"app"</span><span class="p">]]</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>This may not look like much, but it is perhaps the most important
part. I am not an expert on security, but I’m pretty sure that
letting random people on the internet provide commands to be executed
on your server is a bad idea. This code takes a number of precautions:</p>
<ul>
<li>Streams are by invitation only. As we will soon see a random
token will be generated by the channel, and this token will be
placed in the HTML which presumably will be served via <code>https</code>,
so only the recipient can initiate a stream.
</li><li>Even with a token, the only commands that will be issued are
provided by the server, optionally augmented by parameters that
are passed by the client. This code can do further validation
or even provide different commands based on the input provided.
</li><li>The final command is an array of strings allowing the shell
to be bypassed, preventing shell injection attacks.
</li></ul>
<h2 id='part-5-the-channel-itself' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#part-5-the-channel-itself' aria-label='Anchor'></a>Part 5: the channel itself</h2>
<p>Replace <code>app/channels/output_channel.rb</code> with the following:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-w2hs0k2"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-w2hs0k2"><span class="nb">require</span> <span class="s1">'open3'</span>
<span class="k">class</span> <span class="nc">OutputChannel</span> <span class="o"><</span> <span class="no">ApplicationCable</span><span class="o">::</span><span class="no">Channel</span>
<span class="k">def</span> <span class="nf">subscribed</span>
<span class="vi">@stream</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:stream</span><span class="p">]</span>
<span class="vi">@pid</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="n">stream_from</span> <span class="vi">@stream</span> <span class="k">if</span> <span class="vc">@@registry</span><span class="p">[</span><span class="vi">@stream</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">command</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="n">block</span> <span class="o">=</span> <span class="vc">@@registry</span><span class="p">[</span><span class="vi">@stream</span><span class="p">]</span>
<span class="n">run</span><span class="p">(</span><span class="n">block</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">data</span><span class="p">))</span> <span class="k">if</span> <span class="n">block</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">unsubscribed</span>
<span class="no">Process</span><span class="p">.</span><span class="nf">kill</span><span class="p">(</span><span class="s2">"KILL"</span><span class="p">,</span> <span class="vi">@pid</span><span class="p">)</span> <span class="k">if</span> <span class="vi">@pid</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="vc">@@registry</span> <span class="o">=</span> <span class="p">{}</span>
<span class="no">BLOCK_SIZE</span> <span class="o">=</span> <span class="mi">4096</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">register</span><span class="p">(</span><span class="o">&</span><span class="n">block</span><span class="p">)</span>
<span class="n">token</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">base64</span><span class="p">(</span><span class="mi">15</span><span class="p">)</span>
<span class="vc">@@registry</span><span class="p">[</span><span class="n">token</span><span class="p">]</span> <span class="o">=</span> <span class="n">block</span>
<span class="n">token</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">logger</span>
<span class="vi">@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="kp">nil</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">html</span><span class="p">(</span><span class="n">string</span><span class="p">)</span>
<span class="no">Ansi</span><span class="o">::</span><span class="no">To</span><span class="o">::</span><span class="no">Html</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">string</span><span class="p">).</span><span class="nf">to_html</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="n">command</span><span class="p">)</span>
<span class="no">Open3</span><span class="p">.</span><span class="nf">popen3</span><span class="p">(</span><span class="o">*</span><span class="n">command</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">stdin</span><span class="p">,</span> <span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">,</span> <span class="n">wait_thr</span><span class="o">|</span>
<span class="vi">@pid</span> <span class="o">=</span> <span class="n">wait_thr</span><span class="p">.</span><span class="nf">pid</span>
<span class="n">files</span> <span class="o">=</span> <span class="p">[</span><span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">]</span>
<span class="n">stdin</span><span class="p">.</span><span class="nf">close_write</span>
<span class="n">part</span> <span class="o">=</span> <span class="p">{</span> <span class="n">stdout</span> <span class="o">=></span> <span class="s2">""</span><span class="p">,</span> <span class="n">stderr</span> <span class="o">=></span> <span class="s2">""</span> <span class="p">}</span>
<span class="k">until</span> <span class="n">files</span><span class="p">.</span><span class="nf">all?</span> <span class="p">{</span><span class="o">|</span><span class="n">file</span><span class="o">|</span> <span class="n">file</span><span class="p">.</span><span class="nf">eof</span><span class="p">}</span> <span class="k">do</span>
<span class="n">ready</span> <span class="o">=</span> <span class="no">IO</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="n">files</span><span class="p">)</span>
<span class="k">next</span> <span class="k">unless</span> <span class="n">ready</span>
<span class="n">ready</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span>
<span class="n">lines</span> <span class="o">=</span> <span class="n">f</span><span class="p">.</span><span class="nf">read_nonblock</span><span class="p">(</span><span class="no">BLOCK_SIZE</span><span class="p">).</span><span class="nf">split</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
<span class="k">next</span> <span class="k">if</span> <span class="n">lines</span><span class="p">.</span><span class="nf">empty?</span>
<span class="n">lines</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">part</span><span class="p">[</span><span class="n">f</span><span class="p">]</span> <span class="o">+</span> <span class="n">lines</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">unless</span> <span class="n">part</span><span class="p">[</span><span class="n">f</span><span class="p">].</span><span class="nf">empty?</span>
<span class="n">part</span><span class="p">[</span><span class="n">f</span><span class="p">]</span> <span class="o">=</span> <span class="n">lines</span><span class="p">.</span><span class="nf">pop</span><span class="p">()</span>
<span class="n">lines</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span><span class="o">|</span><span class="n">line</span><span class="o">|</span> <span class="n">transmit</span> <span class="n">html</span><span class="p">(</span><span class="n">line</span><span class="p">)}</span>
<span class="k">rescue</span> <span class="no">EOFError</span> <span class="o">=></span> <span class="n">e</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">part</span><span class="p">.</span><span class="nf">values</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">part</span><span class="o">|</span>
<span class="n">transmit</span> <span class="n">html</span><span class="p">(</span><span class="n">part</span><span class="p">)</span> <span class="k">unless</span> <span class="n">part</span><span class="p">.</span><span class="nf">empty?</span>
<span class="k">end</span>
<span class="n">files</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span><span class="o">|</span><span class="n">file</span><span class="o">|</span> <span class="n">file</span><span class="p">.</span><span class="nf">close</span><span class="p">}</span>
<span class="vi">@pid</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="k">rescue</span> <span class="no">Interrupt</span>
<span class="k">rescue</span> <span class="o">=></span> <span class="n">e</span>
<span class="nb">puts</span> <span class="n">e</span><span class="p">.</span><span class="nf">to_s</span>
<span class="k">ensure</span>
<span class="n">files</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span><span class="o">|</span><span class="n">file</span><span class="o">|</span> <span class="n">file</span><span class="p">.</span><span class="nf">close</span><span class="p">}</span>
<span class="vi">@pid</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>There’s a lot here. Let’s start with the public interface:</p>
<ul>
<li><code>self.register</code> is what generates a secure random token and
saves away the block of code that generates the command to
be executed for later use.
</li><li><code>subscribed</code> is called when the stimulus controller creates a subscription.
Note that it will only create a stream if the name of the
stream is in the registry.
</li><li><code>command</code> is what is called when the stimulus controller calls
<code>perform</code>. It will run the block of code from the registry to
determine the command.
</li><li><code>unsubscribed</code> will kill any running process if the cable is closed
</li></ul>
<p>Now the private parts:</p>
<ul>
<li><code>logger</code> will disable the logger. By default Action Cable will
log every request which can produce a lot of output. Feel free
to remove this or filter the output as desired.
</li><li><code>html</code> will call the ANSI to html converter.
</li><li><code>run</code> will launch the command and monitor both the stdout and
stderr streams, reading from them in buffered blocks as output
becomes available, splitting that output in lines and transmitting
those lines as they become available.
</li></ul>
<p>That completes the implementation. Give it a whirl!</p>
<p>Launch your Rails application using <code>bin/dev</code>, and then navigate to
<code>http://localhost:3000/demo/cmd</code>. If you don’t have an existing fly
application to monitor, change the command in <code>app/controllers/demo_controller.rb</code>
to something that will produce output. Perhaps <code>tail -f</code> on a file?</p>
<h2 id='summary' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#summary' aria-label='Anchor'></a>Summary</h2>
<p>Action Cable does the heavy lifting in this scenario. As
<a href='https://api.rubyonrails.org/classes/ActionCable.html' title=''>documented</a> it may
appear daunting and unapproachable, and there appear to be precious few
examples of this kind to learn from, but in practice it can be very easy to
use.</p>
<p>As assembled, there are four pieces to the puzzle. The HTML and Rails
controller are unique to the specific request, and the Stimulus controller
and OutputChannel are designed to be reusable by other requests. In fact
a single application can have multiple scripts and all that would be needed
is HTML and a controller action for each.</p>
<p>My application will have separate scripts for creating and deleting machines,
copying data to volumes, and other administrative tasks. There undoubtedly
will be a few tasks where I will need to drop down to the command line, but
most of the time I’ll be able to do things that used to require my laptop
from my phone.</p>
<p>This example used the output of script commands as the source for real time updates, but other
sources are indeed possible: perhaps a ChatGPT server or stock quotes?
Let your imagination run wild!</p>
<figure class="post-cta">
<figcaption>
<h1>Fly.io ❤️ Rails</h1>
<p>Fly.io is a great way to run your Rails HotWired apps. It’s really easy to get started. You can be running in minutes.</p>
<a class="button bg-white text-violet-500 hover:bg-opacity-75 hover:text-violet-700 transition-colors" href="https://fly.io/docs/rails/">
Deploy a Rails app today! <span class='opacity:50'>→</span>
</a>
</figcaption>
<div class="image-container">
<img src="/static/images/cta-cat.webp" srcset="/static/images/cta-cat@2x.webp 2x" alt="">
</div>
</figure>
/ruby-dispatch/audience-of-one/assets/audience-of-one-thumb.webpAccommodating Safari Usershttps://fly.io/ruby-dispatch/accommodating-safari/2023-10-23T00:00:00+00:002023-11-21T21:08:38+00:00<div class="lead"><p>We’re <a href="http://fly.io/" title="">Fly.io</a>. We run apps for our users on hardware we host around the world. Fly.io happens to be a great place to run Rails applications. Check out how to <a href="https://fly.io/docs/speedrun/" title="">get started</a>!</p>
</div>
<p>When you generate a new Rails app, you are given a choice whether or not you want JavaScript support or to include a CSS framework. The default is to use import maps. What isn’t clear at this time is that there is no documented upgrade path should you change your mind later. In fact, upgrading is difficult as support for things like Turbo, Stimulus, Action Cable and Action Text further lock you into this choice.</p>
<p>Import maps are great until you realize that:</p>
<ul>
<li>Many of your users use MacBooks or iMacs. <em>cool</em>
</li><li>Many of these users don’t choose to upgrade their operating systems <em>understandable</em>
</li><li>Many of these users use Safari <em>ouch</em>
</li></ul>
<p>Why <em>ouch</em>? The problem is that Safari is the only major browser that isn’t <a href='https://thingsthemselves.com/reminder-safari-is-not-an-evergreen-browser/' title=''>evergreen</a>. What that means is that if you use <a href='https://stimulus.hotwired.dev/' title=''>Stimulus</a> things like the following <a href='https://caniuse.com/mdn-javascript_classes_static_class_fields' title=''>will not work</a> for these users of versions of Safari less than 14.1:</p>
<div class="highlight-wrapper group relative javascript">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-4s5fin28"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-4s5fin28"><span class="kd">static</span> <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span> <span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">output</span><span class="dl">"</span> <span class="p">]</span>
</code></pre>
</div>
</div>
<p>And, yes, I do have users on Sierra and High Sierra. Unfortunately even if they upgrade there always will be the possibility that some other JavaScript I write will work on the various browsers that I test with – including recent versions of Safari – just not with the older browsers that they use.</p>
<p>There are ways around this, of course. And there are tools to perform the necessary changes for you automatically. One such tool is <a href='https://esbuild.github.io/' title=''>esbuild</a>. Unfortunately, Rails makes it difficult for you.</p>
<ul>
<li>Despite not being written in JavaScript, Rails makes you buy into the whole Node.js ecosystem if you want to use esbuild. This being despite the fact that no such buy in is required for Tailwindcss despite that tool being written in JavaScript. There is nothing preventing this from being done, and I have done so for <a href='https://github.com/rubys/sprockets-esbuild' title=''>sprockets-esbuild</a>, it is just an explicit choice by the Rails team.
</li><li>Despite Rails import map support being built on sprockets by default, and despite sprockets supporting both transpilation and source maps, import maps explicitly limits inputs to files that don’t require transpilation. Of course it is possible to monkey patch import maps, and I <a href='https://gist.github.com/rubys/82c7477609f561c4c5fdabb462db26d2#file-jsx-import-maps-md' title=''>did so back in 2021</a> and offered this to the Rails team, but once again it was explicitly rejected.
</li></ul>
<p>So that’s the problem in a nutshell. I have a demonstrable need. Going back and switching to esbuild both presents a nearly insurmountable challenge and furthermore is undesirable under the terms the Rails team has provided. Workarounds using sprockets is fragile hack involving monkey patching.</p>
<p>So I came up with another solution. I present <code>lib/tasks/esbuild.rake</code>:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-rma03x3s"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-rma03x3s"> <span class="c1"># minify js controllers and target older browsers</span>
<span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="p">[</span><span class="s1">'assets:precompile'</span><span class="p">].</span><span class="nf">enhance</span> <span class="k">do</span>
<span class="no">Dir</span><span class="p">.</span><span class="nf">chdir</span> <span class="s1">'public/assets/controllers'</span> <span class="k">do</span>
<span class="n">files</span> <span class="o">=</span> <span class="no">Dir</span><span class="p">[</span><span class="s1">'*.js'</span><span class="p">]</span> <span class="o">-</span>
<span class="no">Dir</span><span class="p">[</span><span class="s1">'*.js.map'</span><span class="p">].</span><span class="nf">map</span> <span class="p">{</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="s1">'.map'</span><span class="p">)}</span>
<span class="k">unless</span> <span class="n">files</span><span class="p">.</span><span class="nf">empty?</span>
<span class="n">sh</span> <span class="s2">"esbuild"</span><span class="p">,</span> <span class="o">*</span><span class="n">files</span><span class="p">,</span> <span class="o">*</span><span class="sx">%w(
--outdir=.
--allow-overwrite
--minify
--target=es2020
--sourcemap
)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>What the above does is run esbuild against all of the controller js files that have yet to converted to es2020 and minimized. Sourcemaps are both produced and used to track whether or not the conversion has yet to be done.</p>
<p>While given the constraints, every solution is going to be a bit of a hack, this is the least hacky solution I have come up with to date in that:</p>
<ul>
<li>It defines a <a href='https://guides.rubyonrails.org/v7.1/command_line.html#custom-rake-tasks' title=''>custom rake task</a>
</li><li>It uses the rake <a href='https://ruby.github.io/rake/Rake/Task.html#method-i-enhance' title=''>enhance</a> method
</li><li>It calls <code>esbuild</code> directly.
</li></ul>
<p>Each of these steps are fully documented and supported.</p>
<p>The options used by this script:</p>
<ul>
<li><code>--outdir=.</code> directs that the files be updated in place.
</li><li><code>--allow-overwrite</code> allows the files to be updated in place.
</li><li><code>--minify</code> will reduce whitespace, shorten identifiers, and use equivalent but shorter syntax whenever possible.
</li><li><code>--target=es2020</code> will rewrite js that uses features introduced after this point. Adjust as needed.
</li><li><code>--sourcemap</code> will generate <a href='https://firefox-source-docs.mozilla.org/devtools-user/debugger/how_to/use_a_source_map/index.html' title=''>source map files</a> to enable debugging with the original source
</li></ul>
<p>The end result is scripts that not only will run on older browsers, but because they are smaller they will actually download faster.</p>
<p>I do note that sprockets will also create <code>.gz</code> files. The above script will leave those files alone. As far as I can tell those files aren’t used.</p>
<p>All that is left to be done is to install <code>esbuild</code> separately. This can be done with <code>apt</code>, <code>brew</code>, <code>npm</code> or other techniques. An approach that works well for Dockerfiles:</p>
<div class="highlight-wrapper group relative dockerfile">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-ey6llx1h"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-ey6llx1h"><span class="c"># Install esbuild</span>
<span class="k">RUN </span>chdir /usr/local/bin <span class="o">&&</span> <span class="se">\
</span> curl <span class="nt">-fsSL</span> https://esbuild.github.io/dl/latest | sh
</code></pre>
</div>
</div>
<p>That’s it! Even though it is a bit of a hack, it is a set and forget operation.</p>
<p>Now all of the users of my application can enjoy all the interactivity my stimulus controllers provide.</p>
<figure class="post-cta">
<figcaption>
<h1>Fly.io ❤️ Rails</h1>
<p>Fly.io is a great way to run your Rails HotWired apps. It’s really easy to get started. You can be running in minutes.</p>
<a class="button bg-white text-violet-500 hover:bg-opacity-75 hover:text-violet-700 transition-colors" href="https://fly.io/docs/rails/">
Deploy a Rails app today! <span class='opacity:50'>→</span>
</a>
</figcaption>
<div class="image-container">
<img src="/static/images/cta-kitty.webp" srcset="/static/images/cta-kitty@2x.webp 2x" alt="">
</div>
</figure>
/ruby-dispatch/accommodating-safari/assets/accommodating-safari-thumb.webpBetter Rails Forms with Superformhttps://fly.io/ruby-dispatch/better-rails-forms-with-superform/2023-10-11T00:00:00+00:002023-11-21T21:08:38+00:00<p><strong class='font-[550] text-navy-950'>Rails form helpers have been around for so long that they seem unquestionable, but did you know there’s a way to build forms in Rails that are easier to customize <em>and</em> permit their own strong parameters?</strong></p>
<p><a href='https://github.com/rubymonolith/superform' title=''>Superform</a> is a new form helper library built from scratch on top of <a href='https://www.phlex.fun' title=''>Phlex</a> that makes building forms in Rails less tedious. Here’s what a Superform looks like:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-e6euoekc"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-e6euoekc"><span class="c1"># ./app/views/posts/form.rb</span>
<span class="k">class</span> <span class="nc">Posts::Form</span> <span class="o"><</span> <span class="no">ApplicationForm</span>
<span class="k">def</span> <span class="nf">template</span><span class="p">(</span><span class="o">&</span><span class="p">)</span>
<span class="n">labeled</span> <span class="n">field</span><span class="p">(</span><span class="ss">:title</span><span class="p">).</span><span class="nf">input</span>
<span class="n">labeled</span> <span class="n">field</span><span class="p">(</span><span class="ss">:body</span><span class="p">).</span><span class="nf">textarea</span>
<span class="k">end</span>
<span class="c1"># This would actually be in the ApplicationForm</span>
<span class="c1"># superclass. It's added here for clarity.</span>
<span class="k">def</span> <span class="nf">labeled</span><span class="p">(</span><span class="n">component</span><span class="p">)</span>
<span class="n">div</span> <span class="s2">"p-4"</span> <span class="k">do</span>
<span class="n">render</span> <span class="n">component</span><span class="p">.</span><span class="nf">field</span><span class="p">.</span><span class="nf">label</span>
<span class="n">render</span> <span class="n">component</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>It can then be rendered in Erb, HAML, or any other templating library. Like any view component: create an instance, pass it the <code>@post</code>, and you’ll see a form.</p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-3n5w8cuh"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-3n5w8cuh"><span class="nt"><h1></span>New post<span class="nt"></h1></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="no">Posts</span><span class="o">::</span><span class="no">Form</span><span class="p">.</span><span class="nf">new</span> <span class="vi">@post</span> <span class="cp">%></span>
</code></pre>
</div>
</div>
<p>But the real superpower comes from creating an instance of the form in the controller so it can permit its own parameters.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-8lyp9cko"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-8lyp9cko"><span class="k">class</span> <span class="nc">PostsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="kp">include</span> <span class="no">Superform</span><span class="o">::</span><span class="no">Rails</span><span class="o">::</span><span class="no">StrongParameters</span>
<span class="k">def</span> <span class="nf">create</span>
<span class="vi">@form</span> <span class="o">=</span> <span class="no">Posts</span><span class="o">::</span><span class="no">Form</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="no">Post</span><span class="p">.</span><span class="nf">new</span><span class="p">)</span>
<span class="vi">@post</span> <span class="o">=</span> <span class="n">assign</span> <span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:post</span><span class="p">),</span> <span class="ss">to: </span><span class="vi">@form</span>
<span class="k">if</span> <span class="vi">@post</span><span class="p">.</span><span class="nf">save</span>
<span class="c1"># Success path</span>
<span class="k">else</span>
<span class="c1"># Error path</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Never again will you wonder, “why is my form not updating?” in your Rails development environment only to realize you forgot to add a strong parameter to Rails. 🤦♂️</p>
<h2 id='how-does-superform-permit-parameters' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#how-does-superform-permit-parameters' aria-label='Anchor'></a>How does Superform permit parameters?</h2>
<p>Unlike Rails form helpers, Superform keeps tracks of the field names when the <code>field()</code> method is called in the view where you build the form. For example, in this form below the <code>:title</code> and <code>:body</code> attributes are passed into the <code>field</code> method, which adds them to an internal list of permitted parameters.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-1eup5zym"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-1eup5zym"><span class="c1"># ./app/views/posts/form.rb</span>
<span class="k">class</span> <span class="nc">Posts::Form</span> <span class="o"><</span> <span class="no">ApplicationForm</span>
<span class="k">def</span> <span class="nf">template</span><span class="p">(</span><span class="o">&</span><span class="p">)</span>
<span class="n">labeled</span> <span class="n">field</span><span class="p">(</span><span class="ss">:title</span><span class="p">).</span><span class="nf">input</span>
<span class="n">labeled</span> <span class="n">field</span><span class="p">(</span><span class="ss">:body</span><span class="p">).</span><span class="nf">textarea</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Superform doesn’t actually use Rails strong parameters though—instead the <code>assign</code> method recursively goes through each <code>field</code> defined in the Superform and assigns the corresponding parameter to that field. If the field is not defined, its not assigned.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-wuujmvok"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-wuujmvok"><span class="n">assign</span> <span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:post</span><span class="p">),</span> <span class="ss">to: </span><span class="vi">@form</span>
</code></pre>
</div>
</div>
<p>It’s not really magic, it’s just a form builder that’s both aware of its own structure <em>and</em> visual appearance. Contrast with Rails built-in form builders, which is only capable of describing visual its appearance without knowledge of the fields it’s permitting.</p>
<h2 id='install-superform-in-your-rails-app' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#install-superform-in-your-rails-app' aria-label='Anchor'></a>Install Superform in your Rails app</h2>
<p>If you’re read to get your hands dirty with Superform, add it to your Rails app.</p>
<div class="highlight-wrapper group relative sh">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-rd4z7uac"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-rd4z7uac"><span class="nv">$ </span>bundle add superform
<span class="nv">$ </span>rails g superform:install
</code></pre>
</div>
</div>
<p>This installs Phlex and Superform in your Rails app. You’ll also see the <code>./app/views/application_form.rb</code> file in your Rails project that will look familiar to the forms above. Read the <a href='https://github.com/rubymonolith/superform' title=''>Superform docs</a> to see how the form helpers work and have a look at the <a href='https://www.phlex.fun/' title=''>Phlex website</a> to get a feel for the basics of how you’d build your own HTML components using Ruby.</p>
<h2 id='customize-the-look-and-feel-of-forms' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#customize-the-look-and-feel-of-forms' aria-label='Anchor'></a>Customize the look and feel of forms</h2>
<p>Since Superform is built on top of <a href='https://www.phlex.fun' title=''>Phlex</a>, inputs can be extended to emit the HTML markup your application needs from the <code>ApplicationForm</code> class. Here’s an example where we create a <code>MyInputComponent</code> class that includes inline error messages.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-eljshbsv"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-eljshbsv"><span class="c1"># ./app/views/forms/application_form.rb</span>
<span class="k">class</span> <span class="nc">ApplicationForm</span> <span class="o"><</span> <span class="no">Superform</span><span class="o">::</span><span class="no">Rails</span><span class="o">::</span><span class="no">Form</span>
<span class="k">class</span> <span class="nc">MyInputComponent</span> <span class="o"><</span> <span class="no">ApplicationComponent</span>
<span class="k">def</span> <span class="nf">template</span><span class="p">(</span><span class="o">&</span><span class="p">)</span>
<span class="n">div</span> <span class="ss">class: </span><span class="s2">"form-field"</span> <span class="k">do</span>
<span class="n">input</span><span class="p">(</span><span class="o">**</span><span class="n">attributes</span><span class="p">)</span>
<span class="k">if</span> <span class="n">field</span><span class="p">.</span><span class="nf">errors?</span>
<span class="nb">p</span><span class="p">(</span><span class="ss">class: </span><span class="s2">"form-field-error"</span><span class="p">)</span> <span class="p">{</span> <span class="n">field</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">to_sentence</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="k">class</span> <span class="nc">Field</span> <span class="o"><</span> <span class="no">Field</span>
<span class="k">def</span> <span class="nf">input</span><span class="p">(</span><span class="o">**</span><span class="n">attributes</span><span class="p">)</span>
<span class="no">MyInputComponent</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="nb">self</span><span class="p">,</span> <span class="ss">attributes: </span><span class="n">attributes</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Additionally, methods can be added to the <code>ApplicationForm</code> class that makes common form tasks a bit easier.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-2e6mum8g"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-2e6mum8g"><span class="k">class</span> <span class="nc">ApplicationForm</span>
<span class="k">def</span> <span class="nf">labeled</span><span class="p">(</span><span class="n">component</span><span class="p">)</span>
<span class="n">div</span> <span class="ss">class: </span><span class="s2">"p-4"</span> <span class="k">do</span>
<span class="n">render</span> <span class="n">component</span><span class="p">.</span><span class="nf">field</span><span class="p">.</span><span class="nf">label</span>
<span class="n">render</span> <span class="n">component</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">submit</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
<span class="n">button</span><span class="p">(</span><span class="ss">type: :submit</span><span class="p">)</span> <span class="p">{</span> <span class="n">text</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>These methods can be called from form sub-classes and HTML will be emitted.</p>
<h2 id='wrap-up' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#wrap-up' aria-label='Anchor'></a>Wrap up</h2>
<p><a href='https://github.com/rubymonolith/superform' title=''>Superform</a> is a great way to create highly re-usable and customizable forms that permit their own parameters in Rails.</p>
<p><strong class='font-[550] text-navy-950'>Superform permits its own parameters</strong> - Makes building forms much faster since you don’t have to worry about adding the parameter to the controller while building a form.</p>
<p><strong class='font-[550] text-navy-950'>Forms can be customized with Phlex components</strong> - Superform is built from the ground-up with Phlex components. This makes it easier to mix regular HTML into the helper.</p>
<p>Superform is still a work in progress. It hasn’t reached 1.0 yet and features like <code>select > options</code> are still being developed, but already its being used for sophisticated form use cases, like <a href='https://github.com/rubymonolith/demo/blob/main/app/controllers/blogs/batch/posts_controller.rb' title=''>batch selection UI</a> in a demo app.</p>
/ruby-dispatch/better-rails-forms-with-superform/assets/thumb.webpSQLite & Rails in Productionhttps://fly.io/ruby-dispatch/sqlite-and-rails-in-production/2023-09-12T00:00:00+00:002024-02-05T16:04:11+00:00<p><strong class='font-[550] text-navy-950'>When deploying a Rails application to production, its rare to question the idea of running a Postgres or MySQL database to persist data and Redis for caching, background job processing, and WebSockets, but what if it could all be done without running these services?</strong></p>
<p>“No part is the best part”, right? Here’s the thing, Redis and Postgres run as separate processes that need to be monitored. Usually these processes are running on a separate server that’s accessed over a network connection, which can also break. You’ll either have to deal with that complexity yourself or pay to have it managed with services like <a href='https://fly.io/docs/reference/redis/' title=''>Upstash Redis</a>.</p>
<p>What if we could run <em>everything</em> on one server and not have to worry about “more parts” that can break? You can with SQLite and Litestack.</p>
<p><a href='https://github.com/oldmoe/litestack' title=''>Litestack</a> is a gem that has all the adapters needed to get a Rails application using SQLite for the database, ActiveJob, ActionCable, caching, and more.</p>
<p><a href='https://www.sqlite.org/index.html' title=''>SQLite</a> is a database that stores data in a single file without the need to run a server. In some cases, writing data to <a href='https://www.sqlite.org/fasterthanfs.html' title=''>SQLite is faster than writing files</a> to disk! The <a href='https://corecursive.com/066-sqlite-with-richard-hipp/' title=''>story about how SQLite was designed to work on a battleship</a> is equally as impressive as its implementation.</p>
<p>Combining Litestack and SQLite means you can run an entire Rails application on one box with a multi-process/threaded server, like Puma, and and persist application data, process background jobs, publish WebSocket data, and store cached data.</p>
<p>Here’s how you do it on Fly.io.</p>
<h2 id='install-litestack' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#install-litestack' aria-label='Anchor'></a>Install Litestack</h2>
<p>First, install Litestack in your SQLite Rails application by running:</p>
<div class="highlight-wrapper group relative cmd">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-ucf4ksop"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight cmd'><code id="code-ucf4ksop">bundle add litestack
</code></pre>
</div>
</div>
<p>Then install the adapters and update the configuration files with this Rails generator:</p>
<div class="highlight-wrapper group relative cmd">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-bfx5pvb2"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight cmd'><code id="code-bfx5pvb2">rails g litestack:install
</code></pre>
</div>
</div>
<p>This command configures your Rails environments with the settings they need to use Litestack for ActiveJob, ActionCable, ActionCaching, and your database.</p>
<p>If you don’t have an existing application, but want to try one out, <a href='https://github.com/rubymonolith/demo' title=''>clone the demo blog project</a>, run bundle, and continue below.</p>
<p>Finally let’s deploy it to Fly.io! If you haven’t already, <a href='https://fly.io/docs/hands-on/install-flyctl/' title=''>Install flyctl and create a Fly.io account</a>. Then from the root of your Rails project launch your app.</p>
<div class="highlight-wrapper group relative cmd">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-sp3g6lhq"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight cmd'><code id="code-sp3g6lhq">fly launch
# ... name your app and select the deployment region
</code></pre>
</div>
</div>
<p>This will create a <code>fly.toml</code> file in your project working directory environment, and provision resources on Fly.io. A <code>Dockerfile</code> is also generated, which is a description of the server that will be running in production.</p>
<p>You should see a <code>LITESTACK_DATA_PATH</code> environment in your <code>Dockerfile</code> that points to <code>/data</code>. Litestack uses this path to know which folder to save SQLite databases for caching, job processing, and ActionCable.</p>
<p>Let’s deploy the application and see it run.</p>
<div class="highlight-wrapper group relative cmd">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-a5m24xov"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight cmd'><code id="code-a5m24xov">fly deploy
# ... app deploys
fly open
</code></pre>
</div>
</div>
<p>Congrats! You now have a Rails application running in production that uses SQLite for the database, background job processing, caching, and WebSockets.</p>
<p>Let’s scale up the Fly machine from 256Mb RAM to 512Mb so the Rails app doesn’t run out of memory.</p>
<div class="highlight-wrapper group relative cmd">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-2t22p0af"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight cmd'><code id="code-2t22p0af">fly scale memory 512
</code></pre>
</div>
</div>
<p>If 512 MB of memory is a bit tight for your Rails application, you can always dial it up by running the <code>fly scale memory</code> command with the amount of memory needed to run your app.</p>
<p>When you’re ready to backup your database, run the <code>fly sftp</code> command and it will download the SQLite database file your workstation. Here’s what that looks like.</p>
<div class="highlight-wrapper group relative cmd">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-qolb11xt"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight cmd'><code id="code-qolb11xt">$ fly sftp get /data/production.sqlite3
28672 bytes written to production.sqlite3
$ sqlite3 production.sqlite3
SQLite version 3.39.5 2022-10-14 20:58:05
Enter ".help" for usage hints.
sqlite> .tables
ar_internal_metadata schema_migrations ...
</code></pre>
</div>
</div>
<p>There’s our backup! It doesn’t get much easier than that for database backups. Fly.io is the fastest way to get a SQLite Rails app running in production.</p>
<h2 id='how-does-it-work' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#how-does-it-work' aria-label='Anchor'></a>How does it work?</h2>
<p>Most of the pieces that work with Rails, like ActiveJob, ActiveRecord, etc. ship with an adapter layer that allows developers to implement different backends to fulfill the services job.</p>
<p>The Litestack gem is a library of adapters between these Rails building blocks and SQLite. <a href='https://github.com/oldmoe/litestack/tree/master/lib' title=''>A quick peek inside the Litestack source code</a> and you’ll see the names of familiar Rails services.</p>
<p>When the <code>fly launch</code> command is run, Fly.io runs the <a href='https://github.com/fly-apps/dockerfile-rails' title=''>dockerfile-rails gem</a>, which detects the Litestack gem and configures the <code>Dockerfile</code> with the <code>VOLUME</code> and <code>ENV</code> directives needed to store and access SQLite databases on persistent volume.</p>
<p>The <code>fly launch</code> command then detects the <code>VOLUME</code> directives from the <code>Dockerfile</code> and adds them to the <code>fly.toml</code> file, which configures a persistent volume on the Fly Machine deployment target.</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-dlrz0mqr"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-dlrz0mqr"># Fly adds this volume configuration to the `fly.toml` file.
[[mounts]]
source = "data"
destination = "/data"
</code></pre>
</div>
</div>
<p>Once that’s configured and provisioned, the <code>fly deploy</code> command is run which builds an image from the <code>Dockerfile</code>, configures the volumes, and gets everything running.</p>
<p>That seems like a lot of steps, and it is, but Fly.io does all of that for you so you only have to run <code>fly launch</code> and <code>fly deploy</code> to have a SQLite Rails app running in production.</p>
<h2 id='is-it-fast' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#is-it-fast' aria-label='Anchor'></a>Is it fast?</h2>
<p>Heck yeah it is! Think about it for a moment—your Rails app reads and writes data from a NVMe disk on a physical server. There’s no network connection to traverse; it’s all right there on the same physical machine.</p>
<p><a href='https://github.com/oldmoe/litestack/blob/master/BENCHMARKS.md' title=''>Litestack maintains a set of benchmarks</a> that compares the performance of Litestack vs other libraries. Take it with a grain of salt though, these benchmarks are a baseline that helps understand the relative performance of litestack vs other libraries under conditions that don’t represent a production workload.</p>
<h2 id='limitations' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#limitations' aria-label='Anchor'></a>Limitations</h2>
<p>For small or hobby Rails apps that fit on one server, you really can’t beat using SQLite. It will be faster, easier to maintain, less stuff will break, easier to backup (just download your sqlite file), and easier to upgrade. Like anything though, it comes with trade-offs that are really important to understand before going to production.</p>
<h3 id='what-happens-when-the-server-crashes' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#what-happens-when-the-server-crashes' aria-label='Anchor'></a>What happens when the server crashes?</h3>
<p>Your service goes down! The good news is that you can quickly bring the service back up on a new machine and bind it to the same volume.</p>
<p>As for your data, Fly.io will snapshot it once daily for up to five days of snapshots and of course you can download the SQLite file whenever you want to back it up. You’ll want to understand how Fly.io stores and manages data on volumes, which is <a href='https://fly.io/docs/reference/volumes/' title=''>very well documented</a> including information on how to <a href='https://fly.io/docs/reference/volumes/#restore-from-a-snapshot' title=''>manage snapshots</a>, <a href='https://fly.io/docs/reference/volumes/#access-a-volume' title=''>access a volume</a>, and <a href='https://fly.io/docs/reference/volumes/#extend-a-volume' title=''>make it bigger</a> as your database grows.</p>
<h3 id='but-sqlite-isnt-recommended-for-production-rails-deployments' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#but-sqlite-isnt-recommended-for-production-rails-deployments' aria-label='Anchor'></a>But SQLite isn’t recommended for production Rails deployments!</h3>
<p>It’s true, there’s even a <a href='https://github.com//rails/rails/blob/bd8aeead92c11dbd82ddb9f114ea63b0daf160b4/activerecord/lib/active_record/railtie.rb#L210-L218' title=''>configuration flag for it</a>. Why would this warning be here if running SQLite in production was safe?</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-vm7nlsry"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-vm7nlsry"><span class="no">SQLITE3_PRODUCTION_WARN</span> <span class="o">=</span> <span class="s2">"You are running SQLite in production, this is generally not recommended."</span><span class="p">\</span>
<span class="s2">" You can disable this warning by setting </span><span class="se">\"</span><span class="s2">config.active_record.sqlite3_production_warning=false</span><span class="se">\"</span><span class="s2">."</span>
<span class="n">initializer</span> <span class="s2">"active_record.sqlite3_production_warning"</span> <span class="k">do</span>
<span class="k">if</span> <span class="n">config</span><span class="p">.</span><span class="nf">active_record</span><span class="p">.</span><span class="nf">sqlite3_production_warning</span> <span class="o">&&</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">env</span><span class="p">.</span><span class="nf">production?</span>
<span class="no">ActiveSupport</span><span class="p">.</span><span class="nf">on_load</span><span class="p">(</span><span class="ss">:active_record_sqlite3adapter</span><span class="p">)</span> <span class="k">do</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">logger</span><span class="p">.</span><span class="nf">warn</span><span class="p">(</span><span class="no">SQLITE3_PRODUCTION_WARN</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>There’s a few assumptions that the Rails community makes for contemporary production environments:</p>
<ol>
<li><strong class='font-[550] text-navy-950'>Host file systems are ephemeral</strong> - When deploying Rails applications, we assume either the volumes are “read-only” and a Rails app can’t write to desk <em>or</em> we assume if we can write to disk, it will get replaced on every deploy.
</li><li><strong class='font-[550] text-navy-950'>Databases services are persistent and available from multiple nodes</strong> - When data is written to a data store, we expect it to be there when we ask for it again from any node that’s running on the same cluster.
</li></ol>
<p>When <code>rails new</code> is run, it defaults to the SQLite3 database adapter. This makes setting up a development environment incredibly easy because no additional services need to be installed on the workstation like a database or Redis. This creates a problem though—when people new to Rails deploy the SQLite default to production they might get errors about the volume being read-only or if they’re unlucky, the application would write data that would be deleted on the next deploy. Yikes!</p>
<p>In our case we know we only want to run our Rails application on one node and have taken extra care to store the SQLite data files on a volume that persists between deploys. Since we have taken care of those two foot guns, it’s completely viable to run SQLite in production, so add this to your <code>config/environments/production.rb</code> file to affirm.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-fpergdmm"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-fpergdmm"><span class="c1"># Put this in your config/environments/production.rb file</span>
<span class="n">config</span><span class="p">.</span><span class="nf">active_record</span><span class="p">.</span><span class="nf">sqlite3_production_warning</span>
</code></pre>
</div>
</div><h2 id='wrap-up-the-sqlite-in-production-checklist' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#wrap-up-the-sqlite-in-production-checklist' aria-label='Anchor'></a>Wrap-up: the SQLite in production checklist</h2>
<p>Running a Rails application in production entirely on SQLite is a real possibility. It can lower your applications operational complexity, which will likely save you time and money, and even run faster since application data is stored on the same NVMe disk as the Rails application.</p>
<p>Here’s the checklist to know if your application is suitable for SQLite:</p>
<ol>
<li><strong class='font-[550] text-navy-950'>Runs on one node</strong> - Make sure your Rails application can run on a single node. If running multiple nodes for your application is a requirement, you’ll want to stick with the more traditional client/server database stack.
</li><li><strong class='font-[550] text-navy-950'>Data volumes are writable and persist between deploys</strong> - Make sure the path your SQLite database is writing to doesn’t get wiped out between deploys. Fly will setup a persistent <code>/data</code> Volume for you if it detects you’re running a Rails sqlite application and at least keep your data around between deploys.
</li><li><strong class='font-[550] text-navy-950'>A few seconds of connection queuing between deploys is OK</strong> - When the Rails application is deployed and the server restarts, your application will technically be down. The Fly.io proxy will queue connections until the health checks on the new instance are passing. Once the server is back up and running, the queued connections that haven’t timed out will be sent to the server to fulfill the requests. Your users experience this as their browser taking a few seconds longer to load your website.
</li><li><strong class='font-[550] text-navy-950'>You want to reduce complexity and costs</strong> - Running Fly.io in production requires less servers and monitoring, which can be a great way to keep your application stack simple and costs down.
</li></ol>
<p>Fortunately Fly.io and Litestack provide a reasonable set of defaults that will make your small or hobby Rails app deployable to production without having to worry much about the issues above. When you find your application outgrows running SQLite on one instance, Fly.io is there for the next step with multiple solutions including <a href='https://fly.io/docs/litefs/' title=''>LiteFS</a>, <a href='https://fly.io/docs/postgres/' title=''>Postgres</a>, or <a href='https://fly.io/docs/about/pricing/#apps-v2-and-machines' title=''>bigger machines with more storage, memory, and CPU cores</a>.</p>
/ruby-dispatch/sqlite-and-rails-in-production/assets/sqlite.jpgOrganic Test Driven Developmenthttps://fly.io/ruby-dispatch/organic-test-driven-development/2023-08-21T18:08:10+00:002023-11-21T21:08:38+00:00<p><strong class='font-[550] text-navy-950'>Test-driven development sounds like a really “heavy”, dogmatic way of building applications, but it can be surprisingly organic and lightweight starting with “does it run?” as the first test, followed by more formal unit test verification.</strong></p>
<p>When I set out to write a new form builder library for Rails called <a href='https://github.com/rubymonolith/superform' title=''>Superform</a>, I started with what I wanted the code to look like to the developer who would be building Rails forms with it, so I wrote a little piece of code.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-pqtciiz6"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-pqtciiz6"><span class="no">Superform</span> <span class="ss">:user</span><span class="p">,</span> <span class="ss">object: </span><span class="no">Object</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span>
<span class="n">form</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:name</span><span class="p">)</span>
<span class="n">form</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:email</span><span class="p">)</span>
<span class="n">form</span><span class="p">.</span><span class="nf">collection</span><span class="p">(</span><span class="ss">:addresses</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">address</span><span class="o">|</span>
<span class="n">address</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:street</span><span class="p">)</span>
<span class="n">address</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:city</span><span class="p">)</span>
<span class="n">address</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:state</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>I save that to <code>superform.rb</code> and run it with my first test framework, Ruby!</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-vdzwilw1"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-vdzwilw1"><span class="nv">$ </span>ruby superform.rb
<span class="c"># Superform class not defined</span>
</code></pre>
</div>
</div>
<p>Turns out a unit testing framework is not needed for the very first iterations of test-driven development. If the compiler or interpreter doesn’t run it, the test failed and the compiler or interpreter will say why. If it runs, the tests pass.</p>
<h2 id='minimal-viable-executable-product' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#minimal-viable-executable-product' aria-label='Anchor'></a>Minimal Viable Executable Product</h2>
<p>Once I have what I want the code to look like, I add above it just enough code to get it running in Ruby. In this case I create a classes and a few methods that runs inside Ruby without raising a runtime error.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-oyq7a7ll"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-oyq7a7ll"><span class="k">class</span> <span class="nc">Superform</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">object</span><span class="p">:)</span>
<span class="vi">@key</span> <span class="o">=</span> <span class="n">key</span>
<span class="vi">@object</span> <span class="o">=</span> <span class="n">object</span>
<span class="k">yield</span> <span class="nb">self</span> <span class="k">if</span> <span class="nb">block_given?</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">field</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">collection</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">Superform</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">,</span> <span class="o">&</span><span class="p">)</span>
<span class="no">Superform</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">,</span> <span class="o">&</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">form</span> <span class="o">=</span> <span class="no">Superform</span> <span class="ss">:user</span><span class="p">,</span> <span class="ss">object: </span><span class="no">Object</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span>
<span class="n">form</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:name</span><span class="p">)</span>
<span class="n">form</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:email</span><span class="p">)</span>
<span class="n">form</span><span class="p">.</span><span class="nf">collection</span><span class="p">(</span><span class="ss">:addresses</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">address</span><span class="o">|</span>
<span class="n">address</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:street</span><span class="p">)</span>
<span class="n">address</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:city</span><span class="p">)</span>
<span class="n">address</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:state</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="nb">p</span> <span class="n">form</span>
</code></pre>
</div>
</div>
<p>I run it again from my console.</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-5ruqqzrn"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-5ruqqzrn"><span class="nv">$ </span>ruby superform.rb
<span class="c">#<Superform:0x0000000102fb47c8 @key=:user, @object=#<Object:0x0000000102fb48b8>></span>
</code></pre>
</div>
</div>
<p>No errors! OK, you get the point. You can get pretty far by using the runtime, compiler, or interpreter as the initial test runner.</p>
<p>There will be a point where running Ruby isn’t enough to verify the the code being written does what it is suppose to do—when this point is reached its a good idea to reach for a unit testing framework.</p>
<h2 id='units-tests-dependencies-readme-and-implementation-in-one-file' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#units-tests-dependencies-readme-and-implementation-in-one-file' aria-label='Anchor'></a>Units tests, dependencies, README, and implementation in one file</h2>
<p>When I get Ruby running without error, I add a few things to my file to streamline my next set of iterations.</p>
<ol>
<li><p><strong class='font-[550] text-navy-950'>Dependencies</strong> - The equivalent of a <code>Gemfile</code> can be included in the same file as the unit tests. In my example, I include RSpec. I would also include other gems my library depends on if it were applicable.</p>
</li><li><p><strong class='font-[550] text-navy-950'>Implementation</strong> - The code I started out with above ends up between the dependencies and unit tests. Order matters here since the file is evaluated from top to bottom.</p>
</li><li><p><strong class='font-[550] text-navy-950'>Unit tests</strong> - I prefer RSpec, but you can use any runner that supports running tests from the same file as the implementation. The unit tests should be all of the code that calls your implementation. You can think of it as the public API that demonstrates how others should be using your code.</p>
<p>I also am getting more specific with the data I’m passing to my implementation by changing the object I pass into the <code>object:</code> parameter from <code>Object.new</code> to <code>Struct.new(:name, :email).new("Brad", "brad@example.com")</code>. The <a href='https://docs.ruby-lang.org/en/3.2/Data.html' title=''>Date class</a> in Ruby 3.2 is an excellent way to make objects more concrete.</p>
</li><li><p><strong class='font-[550] text-navy-950'>Problem statement</strong> - At the very bottom of a Ruby file the <code>__END__</code> directive tells Ruby, “this is the end of the file, stop here!”. Below that I like to write the problem my library is trying to solve as clearly as possible. I find if I start going down a tangential rabbit hole, this can bring me back and keep me focused.</p>
</li></ol>
<p>Here’s what it looks like when I put my gem dependencies, implementation, and unit tests together in the same file.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-vtmhwrtu"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-vtmhwrtu"><span class="c1"># Inline bundler resolves dependencies before the application runs.</span>
<span class="nb">require</span> <span class="s1">'bundler/inline'</span>
<span class="n">gemfile</span> <span class="k">do</span>
<span class="n">source</span> <span class="s1">'https://rubygems.org'</span>
<span class="n">gem</span> <span class="s1">'rspec'</span>
<span class="k">end</span>
<span class="nb">require</span> <span class="s2">"rspec/autorun"</span>
<span class="c1"># The implementation of the library I'm working on.</span>
<span class="k">class</span> <span class="nc">Superform</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">object</span><span class="p">:)</span>
<span class="vi">@key</span> <span class="o">=</span> <span class="n">key</span>
<span class="vi">@object</span> <span class="o">=</span> <span class="n">object</span>
<span class="k">yield</span> <span class="nb">self</span> <span class="k">if</span> <span class="nb">block_given?</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">field</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">collection</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">Superform</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">,</span> <span class="o">&</span><span class="p">)</span>
<span class="no">Superform</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">,</span> <span class="o">&</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># Unit tests that describe how my library should work.</span>
<span class="no">RSpec</span><span class="p">.</span><span class="nf">describe</span> <span class="no">Superform</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">{</span> <span class="no">Struct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:email</span><span class="p">).</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Brad"</span><span class="p">,</span> <span class="s2">"brad@example.com"</span><span class="p">)</span> <span class="p">}</span>
<span class="n">subject</span> <span class="k">do</span>
<span class="no">Superform</span> <span class="ss">:user</span><span class="p">,</span> <span class="ss">object: </span><span class="n">user</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span>
<span class="n">form</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:name</span><span class="p">)</span>
<span class="n">form</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:email</span><span class="p">)</span>
<span class="n">form</span><span class="p">.</span><span class="nf">collection</span><span class="p">(</span><span class="ss">:addresses</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">address</span><span class="o">|</span>
<span class="n">address</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:street</span><span class="p">)</span>
<span class="n">address</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:city</span><span class="p">)</span>
<span class="n">address</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:state</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">is_expected</span><span class="p">.</span><span class="nf">to</span> <span class="n">be_instance_of</span><span class="p">(</span><span class="no">Superform</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="c1"># Problems the library solves, which forms the basis of my README.</span>
<span class="cp">__END__
Superform addresses various limitations of Rails form builders including:
1. Superform can permit its own parameters - you don't need to use Strong Parameters if you ...
2. Customize with Phlex components - Rails form builders are somewhat limited in their abilities and force you to constantly switch between Erb and classes...
</span></code></pre>
</div>
</div>
<p>Just like before, I run the file with <code>ruby</code>.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-sg7qwwfy"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-sg7qwwfy"><span class="err">$</span> <span class="n">ruby</span> <span class="n">superform</span><span class="p">.</span><span class="nf">rb</span>
</code></pre>
</div>
</div>
<p>This time I get RSpec output.</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-68y9o5ng"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-68y9o5ng"><span class="nv">$ </span>ruby superform.rb
Superform
example at superform.rb:42 <span class="o">(</span>FAILED - 1<span class="o">)</span>
Failures:
1<span class="o">)</span> Superform
Failure/Error:
def initialize<span class="o">(</span>key, object:<span class="o">)</span>
@key <span class="o">=</span> key
@object <span class="o">=</span> object
yield self <span class="k">if </span>block_given?
end
ArgumentError:
missing keyword: :object
<span class="c"># superform.rb:11:in `initialize'</span>
<span class="c"># superform.rb:21:in `new'</span>
<span class="c"># superform.rb:21:in `collection'</span>
<span class="c"># superform.rb:35:in `block (3 levels) in <main>'</span>
<span class="c"># superform.rb:14:in `initialize'</span>
<span class="c"># superform.rb:26:in `new'</span>
<span class="c"># superform.rb:26:in `Superform'</span>
<span class="c"># superform.rb:32:in `block (2 levels) in <main>'</span>
<span class="c"># superform.rb:42:in `block (2 levels) in <main>'</span>
Finished <span class="k">in </span>0.00056 seconds <span class="o">(</span>files took 0.03982 seconds to load<span class="o">)</span>
1 example, 1 failure
Failed examples:
rspec superform.rb:42 <span class="c"># Superform</span>
</code></pre>
</div>
</div>
<p>Despite failing tests, this is progress! The dependencies for the project and test framework were installed and we now see the output of the tests. The documentation or README at the bottom will keep me focused on the problem I originally set out to solve.</p>
<p>There’s still a lot of work ahead of us to develop the remaining functionality of this library, but we have some awesome guard rails in place that are not too cumbersome.</p>
<h2 id='interactive-development-with-a-repl' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#interactive-development-with-a-repl' aria-label='Anchor'></a>Interactive development with a REPL</h2>
<p>What’s a REPL? Why it’s a Read-eval-print-loop of course! Which explains nothing.</p>
<p>A REPL is a fancy way of saying, “you can interact with your program from a console”. Ruby comes with IRB, which stands for “interactive Ruby”, which is an excellent way to stop your program while running it and see what’s going on with it. <a href='http://pry.github.io/' title=''>Pry</a> is another excellent Ruby REPL, but it has to be installed as a seperate gem.</p>
<p>Anytime you want a REPL to play with the object or debug something, add <code>binding.irb</code> to the context. Let’s add it to line 21 in our program since the spec is failing there.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-oe1b95oj"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-oe1b95oj"> <span class="k">def</span> <span class="nf">collection</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
<span class="nb">binding</span><span class="p">.</span><span class="nf">irb</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>The <code>binding</code> keyword is the context of a program. In this case, we could expand it out to <code>self.binding</code>, which is a way of saying, “give me the context of this program for this instance”. The <code>irb</code> at the end of that tells Ruby to give us an interactive Ruby prompt for the instance. Let’s run it and see what happens.</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-7af34t9d"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-7af34t9d"><span class="nv">$ </span>ruby superform.rb
Superform
From: superform.rb @ line 21 :
16:
17: def field<span class="o">(</span>...<span class="o">)</span>
18: end
19:
20: def collection<span class="o">(</span>...<span class="o">)</span>
<span class="o">=></span> 21: binding.irb
22: self.class.new<span class="o">(</span>...<span class="o">)</span>
23: end
24: end
25:
26: def Superform<span class="o">(</span>key, <span class="k">**</span>kwargs, &<span class="o">)</span>
</code></pre>
</div>
</div>
<p>Ruby shows us where we are in the source code. Let’s try running <code>self.class.new(…)</code> to see what’s wrong.</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-303su466"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-303su466">irb<span class="o">(</span><span class="c">#<Superform:0x00000001052bd5f8>):001:0> self.class.new(...)</span>
superform.rb:11:in <span class="sb">`</span>initialize<span class="s1">': missing keyword: :object (ArgumentError)
from /Users/bradgessler/Projects/sitepress/sitepress/superform.rb:1:in `new'</span>
from /Users/bradgessler/Projects/sitepress/sitepress/superform.rb:1:in <span class="sb">`</span>collection<span class="s1">'
from <internal:prelude>:5:in `irb'</span>
from superform.rb:21:in <span class="sb">`</span>collection<span class="s1">'
from superform.rb:36:in `block (3 levels) in <main>'</span>
from superform.rb:14:in <span class="sb">`</span>initialize<span class="s1">'
from superform.rb:27:in `new'</span>
from superform.rb:27:in <span class="sb">`</span>Superform<span class="s1">'
from superform.rb:33:in `block (2 levels) in <main>'</span>
from /Users/bradgessler/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/rspec-core-3.12.2/lib/rspec/core/memoized_helpers.rb:343:in <span class="sb">`</span>block <span class="o">(</span>2 levels<span class="o">)</span> <span class="k">in </span><span class="nb">let</span><span class="s1">'
... 20 levels...
</span></code></pre>
</div>
</div>
<p>Looks like we’re missing the <code>object:</code> parameter when creating a new instance of the class. Let’s just pass it a generic <code>Object.new</code> for now so we can move on. We might have to come back and change it later.</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-61ji9erf"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-61ji9erf">irb<span class="o">(</span><span class="c">#<Superform:0x00000001052bd5f8>):002:0> self.class.new(:test, object: Object.new)</span>
<span class="o">=></span> <span class="c">#<Superform:0x0000000108674388 @key=:test, @object=#<Object:0x00000001086745b8>></span>
</code></pre>
</div>
</div>
<p>Yup! That works. Now I update the code.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-1rmt4ek5"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-1rmt4ek5"> <span class="k">def</span> <span class="nf">collection</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="ss">object: </span><span class="no">Object</span><span class="p">.</span><span class="nf">new</span><span class="p">)</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="ss">object: </span><span class="n">object</span><span class="p">)</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Then run it to see if the tests pass.</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-dc7ptiy6"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-dc7ptiy6">ruby superform.rb
Superform
is expected to be an instance of Superform
Finished <span class="k">in </span>0.00089 seconds <span class="o">(</span>files took 0.04426 seconds to load<span class="o">)</span>
1 example, 0 failures
</code></pre>
</div>
</div>
<p>Success! I now have RSpec running tests inside of a Ruby script and am in a spot where I can write more tests to get a feel for the public APIs.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-hraw9q82"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-hraw9q82"><span class="c1"># ... our file ...</span>
<span class="no">RSpec</span><span class="p">.</span><span class="nf">describe</span> <span class="no">Superform</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">{</span> <span class="no">Struct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:email</span><span class="p">).</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Brad"</span><span class="p">,</span> <span class="s2">"brad@example.com"</span><span class="p">)</span> <span class="p">}</span>
<span class="n">subject</span> <span class="k">do</span>
<span class="no">Superform</span> <span class="ss">:user</span><span class="p">,</span> <span class="ss">object: </span><span class="n">user</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span>
<span class="n">form</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:name</span><span class="p">)</span>
<span class="n">form</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:email</span><span class="p">)</span>
<span class="n">form</span><span class="p">.</span><span class="nf">collection</span><span class="p">(</span><span class="ss">:addresses</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">address</span><span class="o">|</span>
<span class="n">address</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:street</span><span class="p">)</span>
<span class="n">address</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:city</span><span class="p">)</span>
<span class="n">address</span><span class="p">.</span><span class="nf">field</span><span class="p">(</span><span class="ss">:state</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">is_expected</span><span class="p">.</span><span class="nf">to</span> <span class="n">be_instance_of</span><span class="p">(</span><span class="no">Superform</span><span class="p">)</span> <span class="p">}</span>
<span class="n">describe</span> <span class="s2">"root"</span> <span class="k">do</span>
<span class="n">it</span> <span class="s2">"assigns object"</span> <span class="k">do</span>
<span class="n">expect</span><span class="p">(</span><span class="n">subject</span><span class="p">.</span><span class="nf">object</span><span class="p">).</span><span class="nf">to</span> <span class="n">eql</span> <span class="n">user</span>
<span class="k">end</span>
<span class="n">it</span> <span class="s2">"assigns key"</span> <span class="k">do</span>
<span class="n">expect</span><span class="p">(</span><span class="n">subject</span><span class="p">.</span><span class="nf">key</span><span class="p">).</span><span class="nf">to</span> <span class="n">eql</span> <span class="ss">:user</span>
<span class="k">end</span>
<span class="n">it</span> <span class="s2">"has 2 fields"</span>
<span class="n">it</span> <span class="s2">"has 1 collection"</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>When I run it I see some tests are passing and others are pending.</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-sv9iqcfu"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-sv9iqcfu">ruby superform.rb
Superform
is expected to be an instance of Superform
root
has 2 fields <span class="o">(</span>PENDING: Not yet implemented<span class="o">)</span>
has 1 collection <span class="o">(</span>PENDING: Not yet implemented<span class="o">)</span>
has user object
Pending: <span class="o">(</span>Failures listed here are expected and <span class="k">do </span>not affect your suite<span class="s1">'s status)
1) Superform root has 2 fields
# Not yet implemented
# superform.rb:46
2) Superform root has 1 collection
# Not yet implemented
# superform.rb:47
Finished in 0.00125 seconds (files took 0.04051 seconds to load)
4 examples, 0 failures, 2 pending
</span></code></pre>
</div>
</div>
<p>Now it’s a matter of going back-and-forth between the specs and the implementation to get the whole thing working.</p>
<h2 id='avoid-sunk-cost-bias-by-only-writing-a-few-tests-then-implementing' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#avoid-sunk-cost-bias-by-only-writing-a-few-tests-then-implementing' aria-label='Anchor'></a>Avoid sunk-cost bias by only writing a few tests, then implementing</h2>
<p>At this point it’s tempting to write a bunch of pending or failing tests for the entirety of your application or library, but when you do that you’ll have a much harder time deleting them if you find the design of your code goes in a different direction. Slowly, the test suite will start to feel more like a burden than it is helpful.</p>
<p>To avoid that sunk-cost bias, write a few tests, implement your code against it until the tests pass, commit your work, then rinse and repeat. You’ll find that it’s much easier to pivot the design of your code and do small experiments to see if you like your changes. When you don’t like the changes or direction you wanted to go, its much easier to throw it out and start over by writing a few new tests and implementing against it.</p>
<h2 id='breaking-up-the-file' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#breaking-up-the-file' aria-label='Anchor'></a>Breaking up the file</h2>
<p>Eventually the code needs to be distributed, which means the tests, dependency manifest, and documentation need to be moved out of the same file as the implementation.</p>
<p>In my case I created a gem by running <code>bundle gem superform</code>, then moved the inline RSpec tests into files in the <code>./spec</code> directory, the inline Bundler manifest into the <code>Gemfile</code> and <code>superform.gemspec</code>, and the problem statement into the opening of the <code>README.md</code> file.</p>
<p>The end result is the <a href='https://github.com/rubymonolith/superform' title=''>Superform Github Repo</a> and <a href='https://rubygems.org/gems/superform' title=''>Ruby Gem</a>, where I can continue my iteration by using the gem in my own applications and getting feedback from others who use the gem.</p>
<h2 id='wrap-up' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#wrap-up' aria-label='Anchor'></a>Wrap-up</h2>
<p>The beauty of this approach is you’re not constantly switching between different files. Everything stays in one place when you start out, which keeps things simple and iterations tight.</p>
<p>It’s also a boon for collaboration. If you get stuck and need some help, you have the dependencies, implementation, tests, and problem statement all in one file, which means you could throw it in a gist and post it on Reddit, Mastodon, X, or whatever to get feedback and help.</p>
<p>When you start writing unit tests, only write enough per iteration that you’d be comfortable throwing out if you find your code is heading in a direction you don’t like. Avoid writing massive test suites that you’d find painful to throw out if you find your code is heading in the wrong direction.</p>
<p>As complexity grows and you feel like you’ve nailed an API, you can move the dependencies into its own Gemfile, keep the implementation in its current file, and move the specs out into its own file in the <code>./spec</code> directory.</p>
/ruby-dispatch/organic-test-driven-development/assets/thumb.webpPush to Subscribehttps://fly.io/ruby-dispatch/push-to-subscribe/2023-07-10T00:00:00+00:002023-11-21T21:08:38+00:00<div class="lead"><p>We’re <a href="http://fly.io/" title="">Fly.io</a>. We run apps for our users on hardware we host around the world. Fly.io happens to be a great place to run Rails applications. Check out how to <a href="https://fly.io/docs/speedrun/" title="">get started</a>!</p>
</div>
<p>All your favorite social apps have the ability to send you notifications even
when you aren’t even using them. Sending notifications used to require
different mechanisms for different clients, but with
<a href='https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/?' title=''>Safari</a>
joining the party, now is a good time to consider implementing the <a href='https://caniuse.com/push-api' title=''>Push
API</a> in your applications. This can be a great
way to engage with your audience and clients. </p>
<p>Unfortunately the <a href='https://datatracker.ietf.org/doc/html/draft-ietf-webpush-protocol-12' title=''>webpush protocol</a>, <a href='https://web.dev/push-notifications-web-push-protocol/' title=''>explanations</a>, <a href='https://developer.mozilla.org/en-US/docs/Web/API/Push_API' title=''>API</a>, and even <a href='https://github.com/pushpad/web-push' title=''>gem</a> share a number of problems. Overall, they:</p>
<ul>
<li>provide too many choices
</li><li>are incomplete
</li><li>may even suggest things that no longer work
</li></ul>
<p>This blog post will take you through creating a complete Rails 7 application with Web Push, and will do so in a way that will show you how to add Web Push to your existing Rails application.</p>
<p>This demo application’s model will include Users, Subscriptions, and Notifications, where a User may have both many Subscriptions and many Notifications. We are going to make use of the <a href='https://github.com/pushpad/web-push' title=''>web-push</a> gem and create a <a href='https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API' title=''>service worker</a>.</p>
<p>Before proceeding to the logic, let’s get some scaffolding/administrivia
out of the way. Start by running the following commands:</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-uialq510"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-uialq510">rails new webpush <span class="nt">--css</span><span class="o">=</span>tailwind
<span class="nb">cd </span>webpush
bin/rails generate scaffold User name:string
bin/rails generate scaffold Subscription <span class="se">\</span>
endpoint auth_key p256dh_key user:references
bin/rails generate scaffold Notification <span class="se">\</span>
user:references title:string body:text
bin/rails db:migrate
bundle add web-push
bin/rails generate controller ServiceWorker
</code></pre>
</div>
</div>
<p>Next run the following in the rails console to add some <a href='https://datatracker.ietf.org/doc/html/rfc8292' title=''>VAPID</a> keys to your credentials:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-vmah66fd"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-vmah66fd"><span class="n">creds</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span>
<span class="n">key</span> <span class="o">=</span> <span class="no">Webpush</span><span class="p">.</span><span class="nf">generate_key</span>
<span class="n">add</span> <span class="o">=</span> <span class="no">YAML</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="s1">'webpush'</span> <span class="o">=></span> <span class="n">key</span><span class="p">.</span><span class="nf">to_h</span><span class="p">.</span><span class="nf">stringify_keys</span><span class="p">).</span><span class="nf">sub</span><span class="p">(</span><span class="s1">'---'</span><span class="p">,</span><span class="s1">''</span><span class="p">)</span>
<span class="n">creds</span><span class="p">.</span><span class="nf">write</span> <span class="n">creds</span><span class="p">.</span><span class="nf">read</span> <span class="o">+</span> <span class="s2">"</span><span class="se">\n</span><span class="s2"># Webpush VAPID keys</span><span class="si">#{</span><span class="n">add</span><span class="si">}</span><span class="s2">"</span>
</code></pre>
</div>
</div>
<p>Then add some routes to <code>config/routes.rb</code>:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-r4myme7n"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-r4myme7n"><span class="n">get</span> <span class="s2">"/service-worker.js"</span><span class="p">,</span> <span class="ss">to: </span><span class="s2">"service_worker#service_worker"</span>
<span class="n">post</span> <span class="s2">"/notifications/change"</span><span class="p">,</span> <span class="ss">to: </span><span class="s2">"notification#change"</span><span class="p">,</span>
<span class="ss">as: </span><span class="s2">"change_notifications"</span>
</code></pre>
</div>
</div>
<p>Now let’s get started.</p>
<h2 id='add-a-service-worker' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#add-a-service-worker' aria-label='Anchor'></a>Add a Service Worker</h2>
<p>Web push notifications require you to install some JavaScript that runs
separate from your web application as a <em>service worker</em>. This code has two
responsibilities: listen for push requests and show them as notifications, and post
subscription change information to the server. Place the following into
<code>app/views/service_worker/service_worker.js.erb</code> to accomplish both tasks:</p>
<div class="highlight-wrapper group relative javascript">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-wmsp5ghn"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-wmsp5ghn"><span class="nb">self</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">push</span><span class="dl">"</span><span class="p">,</span> <span class="nx">event</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">title</span><span class="p">,</span> <span class="p">...</span><span class="nx">options</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
<span class="nb">self</span><span class="p">.</span><span class="nx">registration</span><span class="p">.</span><span class="nx">showNotification</span><span class="p">(</span><span class="nx">title</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span>
<span class="p">})</span>
<span class="nb">self</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">pushsubscriptionchange</span><span class="dl">"</span><span class="p">,</span> <span class="nx">event</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">newSubscription</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">newSubscription</span><span class="p">?.</span><span class="nx">toJSON</span><span class="p">()</span>
<span class="nx">event</span><span class="p">.</span><span class="nx">waitUntil</span><span class="p">(</span>
<span class="nx">fetch</span><span class="p">(</span><span class="o"><%=</span> <span class="nx">change_notifications_path</span><span class="p">.</span><span class="nx">inspect</span><span class="p">.</span><span class="nx">html_safe</span> <span class="o">%></span><span class="p">,</span> <span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span> <span class="p">},</span>
<span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span>
<span class="na">old_endpoint</span><span class="p">:</span> <span class="nx">event</span><span class="p">.</span><span class="nx">oldSubscription</span><span class="p">?.</span><span class="nx">endpoint</span><span class="p">,</span>
<span class="na">new_endpoint</span><span class="p">:</span> <span class="nx">event</span><span class="p">.</span><span class="nx">newSubscription</span><span class="p">?.</span><span class="nx">endpoint</span><span class="p">,</span>
<span class="na">new_p256dh</span><span class="p">:</span> <span class="nx">newSubscription</span><span class="p">?.</span><span class="nx">keys</span><span class="p">?.</span><span class="nx">p256dh</span><span class="p">,</span>
<span class="na">new_auth</span><span class="p">:</span> <span class="nx">newSubscription</span><span class="p">?.</span><span class="nx">keys</span><span class="p">?.</span><span class="nx">auth</span>
<span class="p">})</span>
<span class="p">})</span>
<span class="p">)</span>
<span class="p">})</span>
</code></pre>
</div>
</div>
<p>Update <code>app/controllers/service_worker_controller.rb</code> to disable
authentication for this script:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-kyugmpxe"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-kyugmpxe"><span class="k">class</span> <span class="nc">ServiceWorkerController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">skip_before_action</span> <span class="ss">:verify_authenticity_token</span>
<span class="k">def</span> <span class="nf">service_worker</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>There are no secrets in this file, so this is safe to do.</p>
<h2 id='sending-notifications-on-save' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#sending-notifications-on-save' aria-label='Anchor'></a>Sending notifications on save</h2>
<p>For this demo, notifications are stored in the database, and are sent when saved. We accomplish this by using the <code>after_save</code> <a href='https://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks' title=''>Active Record Callback</a> in <code>app/models/notification.rb</code>:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-j1a3qrmw"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-j1a3qrmw"><span class="k">class</span> <span class="nc">Notification</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">belongs_to</span> <span class="ss">:user</span>
<span class="n">after_save</span> <span class="k">do</span> <span class="o">|</span><span class="n">notification</span><span class="o">|</span>
<span class="n">notification</span><span class="p">.</span><span class="nf">user</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">notification</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Next we update the <code>User</code> model in <code>app/models/user.rb</code> to iterate over the subscriptions and call <code>WebPush.payload_send</code> on each. While in this file, we also add <code>has_many</code> calls to complete the relations.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-3ojkdb5c"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-3ojkdb5c"><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">has_many</span> <span class="ss">:notifications</span>
<span class="n">has_many</span> <span class="ss">:subscriptions</span>
<span class="k">def</span> <span class="nf">push</span><span class="p">(</span><span class="n">notification</span><span class="p">)</span>
<span class="n">creds</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span>
<span class="n">subscriptions</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">subscription</span><span class="o">|</span>
<span class="k">begin</span>
<span class="n">response</span> <span class="o">=</span> <span class="no">WebPush</span><span class="p">.</span><span class="nf">payload_send</span><span class="p">(</span>
<span class="ss">message: </span><span class="n">notification</span><span class="p">.</span><span class="nf">to_json</span><span class="p">,</span>
<span class="ss">endpoint: </span><span class="n">subscription</span><span class="p">.</span><span class="nf">endpoint</span><span class="p">,</span>
<span class="ss">p256dh: </span><span class="n">subscription</span><span class="p">.</span><span class="nf">p256dh_key</span><span class="p">,</span>
<span class="ss">auth: </span><span class="n">subscription</span><span class="p">.</span><span class="nf">auth_key</span><span class="p">,</span>
<span class="ss">vapid: </span><span class="p">{</span>
<span class="ss">private_key: </span><span class="n">creds</span><span class="p">.</span><span class="nf">webpush</span><span class="p">.</span><span class="nf">private_key</span><span class="p">,</span>
<span class="ss">public_key: </span><span class="n">creds</span><span class="p">.</span><span class="nf">webpush</span><span class="p">.</span><span class="nf">public_key</span>
<span class="p">}</span>
<span class="p">)</span>
<span class="n">logger</span><span class="p">.</span><span class="nf">info</span> <span class="s2">"WebPush: </span><span class="si">#{</span><span class="n">response</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2">"</span>
<span class="k">rescue</span> <span class="no">WebPush</span><span class="o">::</span><span class="no">ExpiredSubscription</span><span class="p">,</span>
<span class="no">WebPush</span><span class="o">::</span><span class="no">InvalidSubscription</span>
<span class="n">logger</span><span class="p">.</span><span class="nf">warn</span> <span class="s2">"WebPush: </span><span class="si">#{</span><span class="n">response</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2">"</span>
<span class="k">rescue</span> <span class="no">WebPush</span><span class="o">::</span><span class="no">ResponseError</span> <span class="o">=></span> <span class="n">response</span>
<span class="n">logger</span><span class="p">.</span><span class="nf">error</span> <span class="s2">"WebPush: </span><span class="si">#{</span><span class="n">response</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>While this is a fair number of lines of code, it is pretty straightforward, merely passing the notification, the subscription, and VAPID keys we generated earlier to the <code>payload_send</code> call, and the results from this call are logged.</p>
<p>Expired and invalid subscriptions should eventually be cleaned up, but perhaps not immediately as they may be in the process of being changed. As they say, this is left as an exercise for the student.</p>
<h2 id='user-interface' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#user-interface' aria-label='Anchor'></a>User Interface</h2>
<p>We are going to make one functional and one cosmetic change to the user interface for this demo.</p>
<p>Since users have subscriptions, we add a Create Subscription button to the users page in <code>app/views/users/_user.html.erb</code>:</p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-ntbn8dcn"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-ntbn8dcn"><span class="nt"><span</span> <span class="na">data-controller=</span><span class="s">"subscribe"</span> <span class="na">class=</span><span class="s">"hidden"</span>
<span class="na">data-path=</span><span class="cp"><%=</span> <span class="n">subscriptions_path</span> <span class="cp">%></span><span class="err"> </span><span class="s">data-key="</span><span class="cp"><%=</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">webpush</span><span class="p">.</span><span class="nf">public_key</span><span class="p">.</span><span class="nf">tr</span><span class="p">(</span><span class="s2">"_-"</span><span class="p">,</span> <span class="s2">"/+"</span><span class="p">)</span>
<span class="cp">%></span><span class="err">"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="ss">partial: </span><span class="s1">'subscriptions/form'</span><span class="p">,</span> <span class="ss">locals: </span><span class="p">{</span>
<span class="ss">subscription: </span><span class="no">Subscription</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">user: </span><span class="n">user</span><span class="p">)</span>
<span class="p">}</span> <span class="cp">%></span>
<span class="nt"></span></span>
</code></pre>
</div>
</div>
<p>This <code>span</code> element makes use of a Stimulus controller that we will get to in a minute, is initially hidden, contains the <code>subscriptions_path</code> and <code>public_key</code> as data attributes, and renders the subscriptions form with the user pre-filled in.</p>
<p>Where you place this HTML fragment in the form is up to you. If it is inside the <code>if</code> statement, this will only show up on the index page.</p>
<p>The cosmetic change is actually more involved. We start by getting a list of users in NotificationsController in <code>app/controllers/notifications_controller.rb</code>:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-c8ekucwk"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-c8ekucwk"><span class="n">before_action</span> <span class="ss">only: </span><span class="sx">%i[ new edit ]</span> <span class="k">do</span>
<span class="vi">@users</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">all</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span><span class="o">|</span><span class="n">user</span><span class="o">|</span> <span class="p">[</span><span class="n">user</span><span class="p">.</span><span class="nf">name</span><span class="p">,</span> <span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">]}</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Next we make use of this list in <code>app/views/notifications/_form.html.erb</code>:</p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-8cdsu225"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-8cdsu225"><span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">select</span> <span class="ss">:user_id</span><span class="p">,</span> <span class="vi">@users</span><span class="p">,</span> <span class="p">{},</span> <span class="ss">class: </span><span class="s2">"..."</span> <span class="cp">%></span>
</code></pre>
</div>
</div>
<p>And we also make one tiny change to <code>app/views/notifications/_notification.html.erb</code>:</p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-kfadjfdt"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-kfadjfdt"><span class="cp"><%=</span> <span class="n">notification</span><span class="p">.</span><span class="nf">user</span><span class="p">.</span><span class="nf">name</span> <span class="cp">%></span>
</code></pre>
</div>
</div>
<p>Taken together, this lets us create, view, and edit notifications using user names instead of record ids.</p>
<p>If you feel so inclined, you can make the same change to the subscriptions pages, but as that information is lower level and not something that you will be directly editing, it is fine to leave it as it is for now.</p>
<h2 id='wiring-up-the-browser' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#wiring-up-the-browser' aria-label='Anchor'></a>Wiring up the Browser</h2>
<p>At this point, we have a hidden form on user pages. There is a lot left to be done:</p>
<ul>
<li>We have to hide the individual form fields, and then unhide the rest of the form to reveal the Create Subscription button.
</li><li>We disable this button if notifications have already been granted or denied on this device.
</li><li>When the button is clicked we need to request permission for notifications, and if granted do the following in sequence:
<ul>
<li>Register the service worker
</li><li>Create subscription
</li><li>Post the subscription endpoint and keys to the server
</li></ul>
</li></ul>
<p>The code for all of this is below. Place it into
<code>app/javascript/controllers/subscribe_controller.js</code>:</p>
<div class="highlight-wrapper group relative js">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-lisqg1d2"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-lisqg1d2"><span class="k">import</span> <span class="p">{</span> <span class="nx">Controller</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@hotwired/stimulus</span><span class="dl">"</span>
<span class="c1">// Connects to data-controller="subscribe"</span>
<span class="k">export</span> <span class="k">default</span> <span class="kd">class</span> <span class="kd">extends</span> <span class="nx">Controller</span> <span class="p">{</span>
<span class="nx">connect</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">// hide notification form fields</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">field</span> <span class="k">of</span> <span class="k">this</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="dl">'</span><span class="s1">.my-5</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">field</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">none</span><span class="dl">'</span>
<span class="p">}</span>
<span class="c1">// unhide remainder of the form, revealing the submit button</span>
<span class="k">this</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">inline-block</span><span class="dl">'</span>
<span class="c1">// find submit button</span>
<span class="kd">const</span> <span class="nx">submit</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">input[type=submit]</span><span class="dl">'</span><span class="p">)</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">serviceWorker</span> <span class="o">||</span> <span class="o">!</span><span class="nb">window</span><span class="p">.</span><span class="nx">PushManager</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// notifications not supported by this browser</span>
<span class="k">this</span><span class="p">.</span><span class="nx">disable</span><span class="p">(</span><span class="nx">submit</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">Notification</span><span class="p">.</span><span class="nx">permission</span> <span class="o">!==</span> <span class="dl">"</span><span class="s2">default</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// permission has already been granted or denied</span>
<span class="k">this</span><span class="p">.</span><span class="nx">disable</span><span class="p">(</span><span class="nx">submit</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="c1">// prompt for permission when clicked</span>
<span class="nx">submit</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">click</span><span class="dl">"</span><span class="p">,</span> <span class="nx">event</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">event</span><span class="p">.</span><span class="nx">stopPropagation</span><span class="p">()</span>
<span class="nx">event</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">()</span>
<span class="k">this</span><span class="p">.</span><span class="nx">disable</span><span class="p">(</span><span class="nx">submit</span><span class="p">)</span>
<span class="c1">// extract key and path from this element's attributes</span>
<span class="kd">const</span> <span class="nx">key</span> <span class="o">=</span> <span class="nb">Uint8Array</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">atob</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">key</span><span class="p">),</span>
<span class="nx">m</span> <span class="o">=></span> <span class="nx">m</span><span class="p">.</span><span class="nx">codePointAt</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span>
<span class="kd">const</span> <span class="nx">path</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">path</span>
<span class="c1">// request permission, perform subscribe, and post to server</span>
<span class="nx">Notification</span><span class="p">.</span><span class="nx">requestPermission</span><span class="p">().</span><span class="nx">then</span><span class="p">(</span><span class="nx">permission</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">Notification</span><span class="p">.</span><span class="nx">permission</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">granted</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
<span class="nb">navigator</span><span class="p">.</span><span class="nx">serviceWorker</span><span class="p">.</span><span class="nx">register</span><span class="p">(</span><span class="dl">'</span><span class="s1">/service-worker.js</span><span class="dl">'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">registration</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">registration</span><span class="p">.</span><span class="nx">pushManager</span><span class="p">.</span><span class="nx">subscribe</span><span class="p">({</span>
<span class="na">userVisibleOnly</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">applicationServerKey</span><span class="p">:</span> <span class="nx">key</span>
<span class="p">})</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">subscription</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">subscription</span> <span class="o">=</span> <span class="nx">subscription</span><span class="p">.</span><span class="nx">toJSON</span><span class="p">()</span>
<span class="kd">let</span> <span class="nx">formData</span> <span class="o">=</span> <span class="k">new</span>
<span class="nx">FormData</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">form</span><span class="dl">'</span><span class="p">))</span>
<span class="nx">formData</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">subscription[endpoint]</span><span class="dl">'</span><span class="p">,</span>
<span class="nx">subscription</span><span class="p">.</span><span class="nx">endpoint</span><span class="p">)</span>
<span class="nx">formData</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">subscription[auth_key]</span><span class="dl">'</span><span class="p">,</span>
<span class="nx">subscription</span><span class="p">.</span><span class="nx">keys</span><span class="p">.</span><span class="nx">auth</span><span class="p">)</span>
<span class="nx">formData</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">subscription[p256dh_key]</span><span class="dl">'</span><span class="p">,</span>
<span class="nx">subscription</span><span class="p">.</span><span class="nx">keys</span><span class="p">.</span><span class="nx">p256dh</span><span class="p">)</span>
<span class="k">return</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span><span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span>
<span class="dl">'</span><span class="s1">application/x-www-form-urlencoded</span><span class="dl">'</span><span class="p">},</span>
<span class="na">body</span><span class="p">:</span> <span class="k">new</span> <span class="nx">URLSearchParams</span><span class="p">(</span><span class="nx">formData</span><span class="p">).</span><span class="nx">toString</span><span class="p">()</span>
<span class="p">})</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">error</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s2">`Web Push subscription failed: </span><span class="p">${</span><span class="nx">error</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span>
<span class="p">})</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="p">})</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1">// disable the submit button</span>
<span class="nx">disable</span><span class="p">(</span><span class="nx">submit</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">submit</span><span class="p">.</span><span class="nx">removeAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">href</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">submit</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">cursor</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">not-allowed</span><span class="dl">'</span>
<span class="nx">submit</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">opacity</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">30%</span><span class="dl">'</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
</div>
</div>
<p>While the largest block of code in this entire demo, it isn’t particularly
complex: it merely performs a few checks, runs steps in sequence, and
extracts and passes in data as required.</p>
<h2 id='updating-subscriptions' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#updating-subscriptions' aria-label='Anchor'></a>Updating Subscriptions</h2>
<p>One final piece completes the puzzle. Our application needs to update
subscription information as it changes. These requests will be made by
the service worker.</p>
<p>Add the following to <code>app/controllers/subscriptions_controller.rb</code>:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-pog5dd1u"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-pog5dd1u"><span class="c1"># POST /subscriptions/change</span>
<span class="k">def</span> <span class="nf">change</span>
<span class="n">subscription</span> <span class="o">=</span> <span class="no">Subscription</span><span class="p">.</span><span class="nf">find_by_endpoint!</span><span class="p">(</span>
<span class="n">params</span><span class="p">[</span><span class="ss">:old_endpoint</span><span class="p">]</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">params</span><span class="p">[</span><span class="ss">:new_endpoint</span><span class="p">]</span>
<span class="n">subscription</span><span class="p">.</span><span class="nf">endpoint</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:new_endpoint</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">if</span> <span class="n">params</span><span class="p">[</span><span class="ss">:new_p256dh</span><span class="p">]</span>
<span class="n">subscription</span><span class="p">.</span><span class="nf">p256dh_key</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:new_p256dh</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">if</span> <span class="n">params</span><span class="p">[</span><span class="ss">:new_auth</span><span class="p">]</span>
<span class="n">subscription</span><span class="p">.</span><span class="nf">auth_key</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:new_auth</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">if</span> <span class="vi">@subscription</span><span class="p">.</span><span class="nf">save</span>
<span class="nb">format</span><span class="p">.</span><span class="nf">json</span> <span class="p">{</span>
<span class="n">render</span> <span class="ss">:show</span><span class="p">,</span>
<span class="ss">status: :ok</span><span class="p">,</span>
<span class="ss">location: </span><span class="n">subscription</span>
<span class="p">}</span>
<span class="k">else</span>
<span class="nb">format</span><span class="p">.</span><span class="nf">json</span> <span class="p">{</span>
<span class="n">render</span> <span class="ss">json: </span><span class="n">subscription</span><span class="p">.</span><span class="nf">errors</span><span class="p">,</span>
<span class="ss">status: :unprocessable_entity</span>
<span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>For this to work, you will also need to add to the start of the controller:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-8vhste0n"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-8vhste0n"><span class="n">skip_before_action</span> <span class="ss">:verify_authenticity_token</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:change</span><span class="p">]</span>
</code></pre>
</div>
</div>
<p>Endpoints generally are secrets, but it is beyond my expertise to determine how this can be compromised and exploited.</p>
<h2 id='try-it-out' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#try-it-out' aria-label='Anchor'></a>Try it out!</h2>
<p>At this point you should have a working demo. You can launch your server by running <code>bin/dev</code>, create a user by going to <a href="http://localhost:3000/users"><code>http://localhost:3000/users</code></a>. Create a subscription by clicking on the Create Subscription button associated with that user, and finally create a notification by going to <a href="http://localhost:3000/notifications"><code>http://localhost:3000/notifications</code></a>.</p>
<p><a href="http://localhost:3000/subscriptions"><code>http://localhost:3000/subscriptions</code></a> can also be used to see the messy details.</p>
<figure class="post-cta">
<figcaption>
<h1>You can play with this right now.</h1>
<p>It’ll take less than 10 minutes to get your Rails application running globally.</p>
<a class="button bg-white text-violet-500 hover:bg-opacity-75 hover:text-violet-700 transition-colors" href="https://fly.io/docs/rails/">
Try Fly for free <span class='opacity-50'>→</span>
</a>
</figcaption>
<div class="image-container">
<img src="/static/images/cta-rabbit.webp" srcset="/static/images/cta-rabbit@2x.webp 2x" alt="">
</div>
</figure>
<h2 id='explore-further' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#explore-further' aria-label='Anchor'></a>Explore Further</h2>
<p>This blog post can also be viewed as a series of steps that you can use to add push notification to existing Rails applications. Undoubtedly your UI and model will be different, and those changes may affect the stimulus controller, but the basic steps to add the functionality should be the same and hopefully much of this code can be reused.</p>
<p>This demo also only sends <code>title</code> and <code>body</code> for notifications, there are <a href='https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification#parameters' title=''>more options</a> that you can play with.</p>
<p>If you want to explore more, two of the best resources I used in preparing this post were:</p>
<ul>
<li><a href='https://pushpad.xyz/service-worker.js' title=''>pushpad’s service worker code</a>
</li><li><a href='https://www.youtube.com/watch?v=d0XJ9cLfoTs' title=''>Web push with Ruby on Rails</a>
</li></ul>
/ruby-dispatch/push-to-subscribe/assets/push-to-subscribe-thumb.webpPattern Matching on Ruby Objectshttps://fly.io/ruby-dispatch/pattern-matching-on-ruby-objects/2023-07-05T00:00:00+00:002024-02-05T16:04:11+00:00<p><strong class='font-[550] text-navy-950'>Ruby pattern matching landed in 2.7 via the <code>case … in…</code> statement. It’s a very powerful control structure the can make Ruby code cleaner and easier to read.</strong></p>
<p>In this article we’ll explore the basics of how pattern matching can make your Ruby code more readable, then look at how you can implement it on your very own Ruby objects, like an ActiveRecord model or a Ruby class.</p>
<h2 id='the-basics-of-ruby-pattern-matching' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-basics-of-ruby-pattern-matching' aria-label='Anchor'></a>The basics of Ruby pattern matching</h2>
<p>Let’s say you have a method that accepts different hashes and you want to pull data out of certain keys depending on the shape of the data. With pattern matching you can write code that looks like this:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-umjydnrf"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-umjydnrf"><span class="k">def</span> <span class="nf">extract</span><span class="p">(</span><span class="o">**</span><span class="n">data</span><span class="p">)</span>
<span class="k">case</span> <span class="n">data</span>
<span class="k">in</span> <span class="ss">name: </span><span class="p">{</span><span class="n">first</span><span class="p">:}</span>
<span class="nb">puts</span> <span class="n">first</span>
<span class="k">in</span> <span class="ss">tags: </span><span class="p">[</span><span class="n">first_tag</span><span class="p">,</span> <span class="o">*</span><span class="n">_</span><span class="p">]</span>
<span class="nb">puts</span> <span class="n">first_tag</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Now let’s call it with some data and see what we get:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-2jy7mzkb"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-2jy7mzkb"><span class="o">></span> <span class="n">extract</span><span class="p">(</span><span class="ss">name: </span><span class="p">{</span> <span class="ss">first: </span><span class="s2">"Brad"</span><span class="p">,</span> <span class="ss">last: </span><span class="s2">"Gessler"</span> <span class="p">})</span>
<span class="s2">"Brad"</span>
<span class="o">></span> <span class="n">extract</span><span class="p">(</span><span class="ss">tags: </span><span class="p">[</span><span class="s2">"person"</span><span class="p">,</span> <span class="s2">"earthling"</span><span class="p">]</span> <span class="p">})</span>
<span class="s2">"person"</span>
<span class="o">></span> <span class="n">extract</span><span class="p">(</span><span class="ss">name: </span><span class="p">{</span> <span class="ss">first: </span><span class="s2">"Brad"</span><span class="p">,</span> <span class="ss">last: </span><span class="s2">"Gessler"</span> <span class="p">},</span> <span class="ss">tags: </span><span class="p">[</span><span class="s2">"person"</span><span class="p">,</span> <span class="s2">"earthling"</span><span class="p">]</span> <span class="p">})</span>
<span class="s2">"Brad"</span>
</code></pre>
</div>
</div>
<p>Pattern matching makes it possible to write concise, expressive code against data structures in Ruby.</p>
<p>Before pattern matching was released the code above would have had to be written as a complex <code>if... else...</code> statement that does lots of checks on the shape of the data.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-ftaev7vx"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-ftaev7vx"><span class="k">def</span> <span class="nf">matcher</span><span class="p">(</span><span class="o">**</span><span class="n">data</span><span class="p">)</span>
<span class="k">if</span> <span class="n">first</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="ss">:first</span><span class="p">)</span>
<span class="nb">puts</span> <span class="n">first</span>
<span class="k">elsif</span> <span class="n">data</span><span class="p">.</span><span class="nf">key?</span><span class="p">(</span><span class="ss">:tags</span><span class="p">)</span>
<span class="n">tags</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="ss">:tags</span><span class="p">)</span>
<span class="k">if</span> <span class="n">tags</span><span class="p">.</span><span class="nf">is_a?</span> <span class="no">Array</span>
<span class="nb">puts</span> <span class="n">tags</span><span class="p">.</span><span class="nf">first</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>The more complex the data structure, there more conditionals that were needed to check for key existence, data types, etc., which can make code harder to read and more prone to bugs. Thankfully we now have the option of using pattern matching when writing code that checks the “shape” of data in Ruby.</p>
<h2 id='implementing-matchers-on-your-own-ruby-classes' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#implementing-matchers-on-your-own-ruby-classes' aria-label='Anchor'></a>Implementing matchers on your own Ruby classes</h2>
<p>All the examples above use pattern matching against arrays and hashes, but not everything in Ruby is arrays in hashes. How then do we implement pattern matching capabilities on Ruby objects?</p>
<p>Ruby makes it easy to implement pattern matching in your own Ruby gems and code with the <code>desconstruct_keys</code> and <code>deconstruct</code> methods. The <code>deconstruct_keys</code> method returns a Hash object that allows the <code>case</code> statement to match on the hashes key structure while <code>deconstruct</code> returns an Array that can also be used to match an array of values.</p>
<p>Let’s build an HTML request router that uses both to understand when to use each method and how they’re different.</p>
<p>Consider the following HTTP request object. In this example it accepts a path, like <code>/blogs/1/posts</code>, a scheme, like <code>https</code>, and a form, like <code>html</code>.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-c3t1pk96"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-c3t1pk96"><span class="k">class</span> <span class="nc">Request</span> <span class="o"><</span> <span class="no">Data</span><span class="p">.</span><span class="nf">define</span><span class="p">(</span><span class="ss">:path</span><span class="p">,</span> <span class="ss">:scheme</span><span class="p">,</span> <span class="ss">:format</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">deconstruct_keys</span><span class="p">(</span><span class="o">*</span><span class="p">)</span>
<span class="p">{</span> <span class="ss">path: </span><span class="vi">@path</span><span class="p">,</span> <span class="ss">scheme: </span><span class="vi">@scheme</span><span class="p">,</span> <span class="ss">format: </span><span class="vi">@format</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">deconstruct</span><span class="p">(</span><span class="o">*</span><span class="p">)</span>
<span class="n">path</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s2">"/"</span><span class="p">).</span><span class="nf">compact</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Now lets create a request and run it through a router that’s defined with a <code>case … in</code> statement.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-l66w9mmj"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-l66w9mmj"><span class="n">request</span> <span class="o">=</span> <span class="no">Request</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">path: </span><span class="s2">"/blogs/1/posts"</span><span class="p">,</span><span class="ss">scheme: </span><span class="s2">"http"</span><span class="p">)</span>
<span class="k">case</span> <span class="n">request</span>
<span class="k">in</span> <span class="s2">"blog"</span><span class="p">,</span> <span class="n">blog_id</span><span class="p">,</span> <span class="s2">"posts"</span>
<span class="nb">puts</span> <span class="s2">"Blog ID is </span><span class="si">#{</span><span class="n">blog_id</span><span class="si">}</span><span class="s2">"</span>
<span class="k">in</span> <span class="ss">scheme: </span><span class="s2">"http"</span>
<span class="k">raise</span> <span class="s2">"well that's not very secure!"</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>The first <code>case … in</code> statement matches on the array of path segments returned by the <code>deconstruct</code> method. In our case, the deconstruct method splits the request path string from <code>"/blogs/1/posts"</code> into an array <code>["blog", "1", "posts"]</code> that we match in the cast statement to extract the <code>blog_id</code>.</p>
<p>The second <code>case … in</code> statement provides the <code>scheme: "http"</code> expression, which calls the <code>deconstruct_keys</code> method and matches the value <code>"http"</code> for the <code>scheme</code> key. In this case a match on <code>scheme: "http"</code> raises an exception.</p>
<h2 id='conditional-patterns' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#conditional-patterns' aria-label='Anchor'></a>Conditional patterns</h2>
<p>Since <code>deconstruct</code> and <code>deconstruct_keys</code> are just methods on a class, we can have it return different arrays or hashes depending on the state of the class. Let’s add a basic response handler to our example that we can call from the request, depending on the requested format.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-bl9s8crn"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-bl9s8crn"><span class="k">class</span> <span class="nc">Request</span>
<span class="nb">attr_accessor</span> <span class="ss">:path</span><span class="p">,</span> <span class="ss">:scheme</span><span class="p">,</span> <span class="ss">:format</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="ss">path: </span><span class="p">,</span> <span class="ss">scheme: </span><span class="s2">"https"</span><span class="p">,</span> <span class="ss">format: </span><span class="s2">"html"</span><span class="p">)</span>
<span class="vi">@path</span> <span class="o">=</span> <span class="n">path</span>
<span class="vi">@scheme</span> <span class="o">=</span> <span class="n">scheme</span>
<span class="vi">@format</span> <span class="o">=</span> <span class="nb">format</span><span class="p">.</span><span class="nf">to_sym</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">html</span><span class="p">(</span><span class="n">response</span><span class="p">)</span> <span class="o">=</span> <span class="nb">puts</span> <span class="s2">"<p></span><span class="si">#{</span><span class="n">response</span><span class="si">}</span><span class="s2"></p>"</span>
<span class="k">def</span> <span class="nf">json</span><span class="p">(</span><span class="n">response</span><span class="p">)</span> <span class="o">=</span> <span class="nb">puts</span> <span class="sx">%({"data": #{response.to_s.inspect}})</span>
<span class="k">def</span> <span class="nf">deconstruct_keys</span><span class="p">(</span><span class="o">*</span><span class="p">)</span>
<span class="p">{</span> <span class="ss">path: </span><span class="vi">@path</span><span class="p">,</span> <span class="ss">scheme: </span><span class="vi">@scheme</span><span class="p">,</span> <span class="ss">format: </span><span class="vi">@format</span> <span class="p">}.</span><span class="nf">merge</span><span class="p">(</span><span class="n">deconstruct_format</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">deconstruct</span><span class="p">(</span><span class="o">*</span><span class="p">)</span>
<span class="n">path</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s2">"/"</span><span class="p">).</span><span class="nf">compact</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">formatter</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">method</span><span class="p">(</span><span class="vi">@format</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">deconstruct_format</span>
<span class="no">Hash</span><span class="p">[</span><span class="vi">@format</span><span class="p">,</span> <span class="n">formatter</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Let’s implement a server that processes the requests and responds to the request for a given format:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-keu52wxh"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-keu52wxh"><span class="k">def</span> <span class="nf">process</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="k">case</span> <span class="n">request</span>
<span class="k">in</span> <span class="ss">html:
</span><span class="n">html</span><span class="p">.</span><span class="nf">call</span> <span class="s2">"This should be HTML"</span>
<span class="k">in</span> <span class="ss">json:
</span><span class="n">json</span><span class="p">.</span><span class="nf">call</span> <span class="s2">"This should be JSON"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">process</span> <span class="no">Request</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">path: </span><span class="s2">"/blogs/1/posts"</span><span class="p">,</span> <span class="ss">format: </span><span class="s2">"json"</span><span class="p">)</span>
<span class="c1"># "<p>This should be HTML</p>"</span>
<span class="n">process</span> <span class="no">Request</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">path: </span><span class="s2">"/blogs/1/posts"</span><span class="p">,</span> <span class="ss">format: </span><span class="s2">"html"</span><span class="p">)</span>
<span class="c1"># "{data: "This should be JSON"}"</span>
</code></pre>
</div>
</div>
<p>How did we do that!? When the <code>@format = "html"</code> the <code>deconstruct_format</code> method:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-qem7m2pj"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-qem7m2pj"> <span class="k">def</span> <span class="nf">deconstruct_format</span>
<span class="no">Hash</span><span class="p">[</span><span class="vi">@format</span><span class="p">,</span> <span class="n">formatter</span><span class="p">]</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>A hash is created that looks like <code>{ html: self.method(:html) }</code>, which pattern matches on the <code>in html:</code> block in the <code>case</code> statement:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-vn0q3x4f"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-vn0q3x4f"> <span class="k">case</span> <span class="n">request</span>
<span class="k">in</span> <span class="ss">html:
</span><span class="n">html</span><span class="p">.</span><span class="nf">call</span> <span class="s2">"This should be HTML"</span>
</code></pre>
</div>
</div>
<p>That binds the def <code>def html</code> method to the <code>html</code> variable in the statement, which we can then call via <code>html.call</code> and prints <code>"<p>This should be HTML</p>"</code>.</p>
<h2 id='wrap-up' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#wrap-up' aria-label='Anchor'></a>Wrap up</h2>
<p>Pattern matching makes it possible to build even more types of expressive domain specific languages in Ruby that can result in less code. In our examples we created a rudimentary HTTP routing system from a Ruby request object, but we only scratched the surface of Ruby’s pattern matching capabilities.</p>
<p>Here’s a few ideas for how you could use it in your Ruby or Rails applications:</p>
<ul>
<li><strong class='font-[550] text-navy-950'>Permissions, access control, and authorization</strong> - Imagine all the conditionals you’d have to write for an access control object that checks the role on a user, the resources they can access, and the permissions they have on that object. Pattern matching would clean that up making it more understandable.
</li><li><strong class='font-[550] text-navy-950'>HTTP, parameters, middleware, and request/response objects</strong> - In our example above, we used pattern matching against a rudimentary HTTP request object. The same technique could be used within Rack middleware or Rails applications to route requests.
</li><li><strong class='font-[550] text-navy-950'>Interact with JSON APIs</strong> - Some APIs return JSON objects that are “different shapes” or deeply nested. Extracting this data can sometimes take lots of explicit conditionals that check if a key is present before accessing it. Pattern matching will handle all of that checking for you implicitly making it easier for others to follow the intent of the code.
</li></ul>
<p>It’s definitely worth 15 minutes of your time reading carefully through the Ruby pattern matching documentation at <a href='https://docs.ruby-lang.org/en/master/syntax/pattern_matching_rdoc.html' title=''>https://docs.ruby-lang.org/en/master/syntax/pattern<em>matching</em>rdoc.html</a> if you’re interested in writing more concise expressive Ruby code. It covers much more than this article including <a href='https://docs.ruby-lang.org/en/master/syntax/pattern_matching_rdoc.html#label-Variable+pinning' title=''>variable pinning</a>, <a href='https://docs.ruby-lang.org/en/master/syntax/pattern_matching_rdoc.html#label-Guard+clauses' title=''>guard clauses</a>, <a href='https://docs.ruby-lang.org/en/master/syntax/pattern_matching_rdoc.html#label-Variable+binding' title=''>variable binding</a>, and more. There’s even a <a href='https://docs.ruby-lang.org/en/master/syntax/pattern_matching_rdoc.html#label-Appendix+A.+Pattern+syntax' title=''>handy cheatsheet</a> that includes all the parts of Ruby pattern matching that’s a great reference once you’ve mastered pattern matching techniques.</p>
/ruby-dispatch/pattern-matching-on-ruby-objects/assets/thumb.webpAround the World With SQLite3 and Rsynchttps://fly.io/ruby-dispatch/smooth-fly-dev/2023-06-22T00:00:00+00:002024-02-05T16:04:11+00:00<div class="lead"><p><a href="http://fly.io/" title="">Fly.io</a> runs apps close to users around the world. This same infrastructure can be used to route requests to where the data resides. <a href="https://fly.io/docs/speedrun/" title="">Give us a whirl</a> and get up and running quickly.</p>
</div>
<p>Take a typical Rails application, run <code>fly launch</code>, then <code>fly deploy</code>, say yes a few times and you will have a Dockerfile provided for you, with two instances of your application up and running, a PostgreSQL database, and an Upstash Redis database.</p>
<p>This is a great default, but should you desire to do so you are welcome to configure your application however you want. This can be done by updating your Dockerfile yourself, but you will rarely need to resort to this as <a href='https://github.com/fly-apps/dockerfile-rails' title=''>dockerfile-rails</a> provides lots of options to help with this process.</p>
<p>This blog post will take you through the configuration of the <a href='https://github.com/rubys/showcase#showcase' title=''>showcase</a> application. Highlights:</p>
<ul>
<li>Each event is a separate instance of the same Rails application, with a separate sqlite3 database, and a separate active storage directory, both on a mounted <a href='https://fly.io/docs/reference/volumes/' title=''>volume</a>.
</li><li>Multiple events in the same region run on the same machine using <a href='https://www.phusionpassenger.com/' title=''>Phusion Passenger</a> and share the same Action Cable process and redis instance.
</li><li>Requests are <a href='https://fly.io/docs/reference/dynamic-request-routing/' title=''>dynamically routed</a> to the machine nearest to the event, and data is synchronized between machines using <a href='https://rsync.samba.org/' title=''>rsync</a>.
</li><li>I’m not sure I’m going to keep it, but each machine is also running sshd enabling me to syncronize the content with machines outside of fly.
</li></ul>
<p>This application is running at <a href='https://smooth.fly.dev/showcase/' title=''>smooth.fly.dev</a>. You are welcome to see the index, but access to the individual event pages are password protected as they contain customer names, invoicing information, and scores.</p>
<p>While a large number of techniques will be shown below, your needs are undoubtedly different, but hopefully these examples will inspire you to make your own customizations.</p>
<h2 id='starting-with-a-single-event' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#starting-with-a-single-event' aria-label='Anchor'></a>Starting with a single event</h2>
<p>We are going to start with one event, but in the process we will
prepare for multiple events and multiple regions. This can be done using the following steps:</p>
<ul>
<li><p>Create a volume, and mount it. <code>fly launch</code> will do this automatically for Rails applications unless you select PostgreSQL.</p>
</li><li><p>Set <code>DATABASE_URL</code> to <code>sqlite3:///data/db/2022-harrisburg.sqlite3</code>. This places the database on the volume, with a unique name per event. This can be done via <code>bin/rails generate dockerfile env=DATABASE_URL:sqlite3:///data/db/2022-harrisburg.sqlite3</code></p>
</li><li><p>Set <code>RAILS_STORAGE</code> to <code>/data/storage/2022-harrisburg</code>, and update <code>config/storage.yml</code> as follows:</p>
<p><pre> local:
service: Disk
public: true
root: <%= ENV.fetch('RAILS_STORAGE',
Rails.root.join("storage")) %></pre></p>
<p>This will place active storage files on the volume, again in a separate
location per event.</p>
</li><li><p>Install, configure, and launch redis. This requires a number of sub-steps:</p>
<ul>
<li><p>Create a <code>Procfile.fly</code> with the following contents:</p>
<p><pre>
web: bin/rails server
redis: redis-server /etc/redis/redis.conf </pre></p>
<p>This will launch the rails and redis servers in separate processes.</p>
</li><li><p>Create <code>config/deploy.fly</code> with the following contents:</p>
<p><pre style="white-space: pre">
# configure redis
RUN sed -i 's/^daemonize yes/daemonize no/' /etc/redis/redis.conf &&\
sed -i 's/^bind/# bind/' /etc/redis/redis.conf &&\
sed -i 's/^protected-mode yes/protected-mode no/' /etc/redis/redis.conf &&\
sed -i 's/^logfile/# logfile/' /etc/redis/redis.conf &&\
echo "vm.overcommit_memory = 1" >> /etc/sysctl.conf </pre></p>
<p>Configuration of redis is beyond the scope of this blog post, but
hopefully the above example makes clear that any Dockerfile
instructions can be included.</p>
</li><li><p>Run the following command to update your Dockerfile:</p>
<p><pre>
bin/rails generate dockerfile --add=redis \
--procfile=Procfile.fly --instructions=config/deploy.fly</pre></p>
</li></ul>
</li></ul>
<p>While the above is indeed a fair amount of preparation, it illustrates how you
can set environment variables, add packages, run multiple processes, and even
add custom instructions to your Dockerfile in a way that retains the ability to
regenerate the remaining portions of your Dockerfile at any time.</p>
<figure class="post-cta">
<figcaption>
<h1>You can play with this right now.</h1>
<p>It’ll take less than 10 minutes to get your Rails application running globally.</p>
<a class="button bg-white text-violet-500 hover:bg-opacity-75 hover:text-violet-700 transition-colors" href="https://fly.io/docs/rails/">
Try Fly for free <span class='opacity-50'>→</span>
</a>
</figcaption>
<div class="image-container">
<img src="/static/images/cta-turtle.webp" srcset="/static/images/cta-turtle@2x.webp 2x" alt="">
</div>
</figure>
<h2 id='add-additional-events-in-the-same-region' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#add-additional-events-in-the-same-region' aria-label='Anchor'></a>Add additional events in the same region</h2>
<p>While the Phusion Passenger documentation for <a href='https://www.phusionpassenger.com/library/dev/ruby/multitenancy_and_microservices.html' title=''>developing multiple applications and microservices with Passenger + Nginx</a> isn’t written yet, it really is just a matter of following the instructions for <a href='https://www.phusionpassenger.com/library/deploy/nginx/deploy/ruby/#deploying-an-app-to-a-sub-uri-or-subdirectory' title=''>deploying an app to a sub-URI or subdirectory</a>, and repeating this step as many times as necessary.</p>
<p>Once again, this involves multiple discrete steps. In the showcase application it starts with a <a href='https://github.com/rubys/showcase/blob/main/config/tenant/showcases.yml' title=''>showcases.yml</a> file containing the list of events and an <a href='https://github.com/rubys/showcase/blob/main/config/tenant/nginx-config.rb' title=''>nginx-config</a> script which generates the nginx configuration from this data and places the results into the <code>/etc/nginx/sites-enabled</code> directory.</p>
<p>This script also has another responsibility: it runs <code>db:prepare</code> (and therefore <code>db:migrate</code>) against each of the databases in this region.</p>
<p>This also implies that the right time to run this script is in place of the rails <code>db:prepare</code> script. Doing both, namely installing passenger and reconfiguring what script is run at startup, can be done with a single command:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-otmqbc5b"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-otmqbc5b">bin/rails generate dockerfile --passenger \
--migrate=config/tenant/nginx-config.rb
</code></pre>
</div>
</div>
<p>One last change needs to be made. In <code>Procfile.fly</code> we need to run <code>nginx</code> instead of <code>bin/rails server</code>:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-v8465vs4"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-v8465vs4">web: nginx
redis: redis-server /etc/redis/redis.conf
</code></pre>
</div>
</div><h2 id='events-in-multiple-regions' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#events-in-multiple-regions' aria-label='Anchor'></a>Events in multiple regions</h2>
<p>Much of this step builds on concepts in previous steps.</p>
<p>We already have a script that builds an nginx configuration file. Fly machines have a number of <a href='https://fly.io/docs/reference/runtime-environment/' title=''>environment variables</a> set. We will make use of <code>FLY_REGION</code> and <code>PRIMARY_REGION</code>.</p>
<p>In that generated nginx configuration we will produce <a href='https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409' title=''>HTTP 409</a> responses that add the <a href='https://fly.io/docs/reference/dynamic-request-routing/#the-fly-replay-response-header' title=''>FLY-Replay</a> header, thus:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-by6mtk0d"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-by6mtk0d"># Charlotte
location /showcase/2023/charlotte {
return 409 "wrong region\n";
add_header Fly-Replay region=atl always;
}
# Chicago
location /showcase/2023/chicago {
return 409 "wrong region\n";
add_header Fly-Replay region=ord always;
}
# Clearwater - Glass Slipper Gala
location /showcase/2023/clearwater/glassslipper {
return 409 "wrong region\n";
add_header Fly-Replay region=mia always;
}
</code></pre>
</div>
</div>
<p>Events that are in the current <code>FLY_REGION</code> continue to contain passenger
directives. This means that the nginx configuration file is different in
each region. <a href='./showcase.conf' title=''>showcase.conf</a> contains an example of a
full generated configuration.</p>
<p>Perhaps more interesting is the use of <a href='https://rsync.samba.org/' title=''>rsync</a> and
<a href='https://www.openssh.com/' title=''>openssh</a> to initially load and synchronize data.
Installation is done via passing <code>--add rsync openssh-server</code> to the generate
dockerfile command. Configuration is done by
<a href='https://github.com/rubys/showcase/blob/main/config/deploy.fly' title=''>config/deploy.fly</a>.
Next the migration script is changed to
<a href='https://github.com/rubys/showcase/blob/main/bin/deploy' title=''>bin/deploy</a> which will
load the volume with data from the primary region during startup. A few notes:</p>
<ul>
<li>Configuration is a matter of mapping directories to user ids, and allowing all accesses. As the port is not exposed to the external internet, this is safe.
</li><li>As the rsync server is started as a daemon it can’t be launched by a procfile. Instead it is launched by the deploy/migrate script.
</li><li>Rsync is called on both the db and storage subdirectories, passing the <code>--update</code> flag indicating that only files that are newer in the primary region are to be copied.
</li></ul>
<p>Finally, a
<a href='https://www.phusionpassenger.com/library/indepth/hooks.html#detached_process' title=''>detached_process</a>
<a href='https://github.com/rubys/showcase/blob/main/bin/passenger-hook' title=''>passenger
hook</a> is
defined which is called after <a href='https://www.phusionpassenger.com/library/config/nginx/reference/#passenger_pool_idle_time' title=''>300 seconds of idle
time</a>.
This hook script checks how many processes remain and when there are no
non-cable processes left it will use <a href='#' title=''>dig</a> commands to identify other <a href='https://fly.io/docs/reference/private-networking/#fly-internal-addresses' title=''>app
instances</a>
and call rsync to copy all files to them. Again the <code>--update</code> flag is specified so that only files that are newer on the source will be copied to the receiver.</p>
<p>Rsync between fly machines runs quite fast, and running it when machines are idle means that eventually all machines have complete copies of all databases. In the future I may refine this strategy but as the databases are small (typically one megabyte or less), the comfort of knowing that there are multiple backups and the regions can be reconfigured at will outweigh the concerns of the additional disk space required.</p>
<p>Separately, ssh is also set up to enable machines outside of the fly network to rsync automatically rsync data to and from volumes. Placing lines like the following in <code>~/.ssh/config</code> makes it easy:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-mn43mldt"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-mn43mldt">Host smooth
HostName smooth.fly.dev
Port 2222
User rails
Host mia.smooth
HostName mia.smooth.internal
ProxyJump rails@smooth.fly.dev:2222
Port 2222
User rails
</code></pre>
</div>
</div><h2 id='recap' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#recap' aria-label='Anchor'></a>Recap</h2>
<p>The joy of this setup is that provisioning a new regions is as easy as
running <a href='https://fly.io/docs/flyctl/machine-clone/' title=''>flyctl machine clone</a> passing the name of the new region. Adding events to existing or new regions is merely a matter of updating <a href='https://github.com/rubys/showcase/blob/main/config/tenant/showcases.yml' title=''>showcase.yml</a> and running <code>fly deploy</code>.</p>
<p>I’m also continuing to <a href='https://rubix.intertwingly.net/showcase/docs/ops/Hosting' title=''>host this application both at home and on Hetzner</a>. The experience I have gained from each has lead to numerous improvements in making dockerfile-rails work with fly.io. I still need to work out synchronization of authentication and do more testing before I make fly.io the primary for future events, but that will happen soon.</p>
<p>As with everything distributed, a number of engineering trade-effs are involved, specifically:</p>
<ul>
<li>all static files (css, js, images) as well as the index page are all served from the machine closest to the requester.
</li><li>accesses (both read and write) from users near to events will be fast.
</li><li>accesses (even read-only) from users distant from events will be routed to the machine hosting the event.
</li><li>while scaling CPUs and RAM is possible, creating a second machine in the same region is not supported.
</li><li>deploying changes will require momentary downtime.
</li></ul>
<p>This blog post covered a lot of ground. It contained a mix of step by step
instructions and a description and pointers to running code. It is not
expected that others will mimic exactly this setup, but hopefully seeing how
this application was set up will inspire others to configure their own network
of machines using
<a href='https://github.com/fly-apps/dockerfile-rails' title=''>dockerfile-rails</a>. Should there
be interest, similar functionality can be added to
<a href='https://github.com/fly-apps/dockerfile-node' title=''>dockerfile-node</a> and, over time,
spread to other frameworks.</p>
<p>Finally, take a peek at <a href='https://github.com/rubys/showcase/blob/main/config/dockerfile.yml' title=''>config/dockerfile.yml</a> to see the complete list of options I use, as well as the resulting <a href='https://github.com/rubys/showcase/blob/main/Dockerfile' title=''>Dockerfile</a>. If you have an interesting use case or set of options that believe may be useful to others, start a <a href='https://github.com/fly-apps/dockerfile-rails/discussions' title=''>discussion</a>, open an <a href='https://github.com/fly-apps/dockerfile-rails/issues' title=''>issue</a> or make a <a href='https://github.com/fly-apps/dockerfile-rails/pulls' title=''>pull request</a>.</p>
<p>And you can always use <a href='https://community.fly.io/' title=''>community.fly.io</a> for more generic, or fly.io specific questions.</p>
/ruby-dispatch/smooth-fly-dev/assets/smooth-fly-dev-thumb.webpRubyKaigi 2023: Matsumotohttps://fly.io/ruby-dispatch/ruby-kaigi-2023/2023-05-31T00:00:00+00:002024-02-05T16:04:11+00:00<p>The <a href='https://rubykaigi.org/2023/' title=''>RubyKaigi</a> conference was in Matsumoto this year, surrounded by beautiful mountains in Nagano Prefecture. It’s an annual conference in Japan that brings Rubyists from around the world to celebrate Ruby and the community.</p>
<p>I ate a lot of local soba.</p>
<p>Shout out to <a href='https://visitmatsumoto.com/wp-content/uploads/2018/07/cs_localfood_img_large02.jpg' title=''>sanzokuyaki</a>— a local fried chicken cuisine.</p>
<p>Okay. Let’s talk about RubyKaigi and some of themes of the conference.</p>
<div class="callout"><p>Note: At the time of writing, videos from the conference are not available yet.</p>
</div><h2 id='ruby-is-30-years-old' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#ruby-is-30-years-old' aria-label='Anchor'></a>Ruby is 30 years old</h2>
<p>Matz opened the conference with a keynote that reminded us of how we got here. This year is the 30th anniversary Ruby. 🍾 おめでとうございます。 お疲れ様でした。</p>
<h2 id='yjit-is-here-but-also-its-still-coming' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#yjit-is-here-but-also-its-still-coming' aria-label='Anchor'></a>YJIT is here (but also it’s still coming)</h2>
<p><a href='https://github.com/Shopify/yjit' title=''>YJIT is a the just-in-time compiler written in Rust</a> that Shopify has been spearheading for a number of years now. Maxime Chevalier-Boisvert <a href='https://rubykaigi.org/2023/presentations/maximecb.html#day2' title=''>delivered a key note summarizing the history of the project</a> and shared some surprising benchmarks. Shopify, which runs the self-proclaimed biggest Rails production app (and I don’t doubt it) is already seeing the benefits. They’ve seen performance gains that have well exceeded the original goal of 10-15% speed up. Sometimes up to x3 performance.</p>
<p>I strongly suggest watching Maxime’s keynote when it’s made available.</p>
<p>👋 If you’re interested in seeing some of the current YJIT benchmarks, head over to <a href='https://speed.yjit.org/' title=''>https://speed.yjit.org/</a></p>
<div class="callout"><p>You can add YJIT to your Rails project when creating a docker file.</p>
</div><div class="highlight-wrapper group relative sh">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-f3snp7qk"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-f3snp7qk"><span class="c"># from the root directory of a Rails project</span>
fly launch
bin/rails generate dockerfile <span class="nt">--yjit</span>
fly deploy
</code></pre>
</div>
</div><h2 id='ractor-works-but-isnt-popular-yet' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#ractor-works-but-isnt-popular-yet' aria-label='Anchor'></a>Ractor works, but isn’t popular yet</h2>
<p><a href='https://github.com/ruby/ruby/blob/master/doc/ractor.md' title=''>Ractor is Ruby’s lastest answer to concurrency, but more importantly parallelism.</a> It was introduced in Ruby 3.0.</p>
<ul>
<li>It works! But there’s a lot of room for improvement. This includes an effort to make use M:N threading model to have more control over scheduling than the current implementation that relies on native threads. Checkout Koichi’s talk from 2022 called <a href='https://rubykaigi.org/2022/presentations/ko1.html' title=''>“Making <em>**M</em><em>a</em><em>N</em><em>y</em> threads on Ruby”</a>
</li><li>It’s not popular, yet. There are plans to improve the Ractor API to make it more friendly for developers. Combined with the potential performance benefits it will hopefully attract a wave of new development
</li></ul>
<p>Talks:</p>
<ul>
<li><a href='https://rubykaigi.org/2023/presentations/ko1.html#day1' title=''>“Ractor” reconsidered</a> by Koichi <strong class='font-[550] text-navy-950'>Sasada</strong>
</li><li><a href='https://rubykaigi.org/2023/presentations/m_seki.html#day2' title=''>Learn Ractor</a> by Masatoshi SEKI
</li></ul>
<h2 id='universal-parser-is-coming-maybe' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#universal-parser-is-coming-maybe' aria-label='Anchor'></a>Universal Parser is coming, maybe</h2>
<p>There’s a lot of effort happening around Ruby parsers:</p>
<ul>
<li><a href='https://rubykaigi.org/2023/presentations/spikeolaf.html#day1' title=''>The future vision of Ruby Parser</a> by Yuichiro Kaneko
</li><li><a href='https://rubykaigi.org/2023/presentations/coe401_.html#day2' title=''>Implementing “++” operator, stepping into parse.y</a> by Masaki Shioi
</li><li><a href='https://rubykaigi.org/2023/presentations/kddnewton.html#day2' title=''>Yet Another Ruby Parser</a> by Kevin Newton
</li></ul>
<p>In <a href='https://rubykaigi.org/2023/presentations/rubylangorg.html#day3' title=''>Ruby Commiters and The World</a>, the opening talk on day 3, there were some hints from Matz that maybe Ruby would one day have one de facto universal parser, but it’s not something he’s considering in the short term. Maybe in Ruby 3.4, but don’t get your hopes up.</p>
<h2 id='types-everyones-favorite-ruby-feature' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#types-everyones-favorite-ruby-feature' aria-label='Anchor'></a>Types: Everyone’s favorite Ruby feature?</h2>
<p>Okay, Types aren’t exactly the first thing I think about when it comes to Ruby, but I guess they are now:</p>
<ul>
<li><a href='https://rubykaigi.org/2023/presentations/egiurleo.html#day1' title=''>Generating RBI’s for Dynamic mixins with Sorbet and Tapioca</a> by Emily Samp
</li><li><a href='https://rubykaigi.org/2023/presentations/tompng.html#day1' title=''>Power up your REPL life with types</a> by Tomoya Ishida
</li><li><a href='https://rubykaigi.org/2023/presentations/Morriar.html#day3' title=''>Gradual typing for Ruby: comparing RBS and RBI/Sorbet</a> by Alexandre Terrasa
</li><li><a href='https://rubykaigi.org/2023/presentations/p_ck_.html#day3' title=''>Let’s write RBS!</a> by Masataka Kuwabara
</li><li><a href='https://rubykaigi.org/2023/presentations/soutaro.html#day3' title=''>Parsing RBS</a> by Soutaro Matsumoto
</li></ul>
<p>Shopify uses Sorbet and various type tools/libraries in production. Alexandre Terrasa’s presentation contained some interesting polling data from Shopify developers that showed an increased adoption/satisfaction with types in Ruby/Rails.</p>
<h2 id='ruby-is-not-dead' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#ruby-is-not-dead' aria-label='Anchor'></a>Ruby is not dead</h2>
<p>This was a theme of last year’s conference, but just to drive the point home checkout the long list of sponsors this year: <a href='https://rubykaigi.org/2023/sponsors/' title=''>https://rubykaigi.org/2023/sponsors/</a></p>
<h2 id='ruby-3-3-has-a-lot-of-promise' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#ruby-3-3-has-a-lot-of-promise' aria-label='Anchor'></a>Ruby 3.3 has a lot of promise</h2>
<p>Ruby 3.3.0‑preview1 was released during the conference. The new version looks to hold a lot of promise due to the effort of the core team/community, the YJIT project, efforts around a universal parser, and efforts to improve Ractor. Ruby is projected to be faster and more maintainable.</p>
<h2 id='honorable-mentions' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#honorable-mentions' aria-label='Anchor'></a>Honorable Mentions</h2>
<p>This post was about themes of the conference more generally, but there were a lot of other talks that aren’t mentioned in this post (I know I certainly have a list of personal favorites). I highly suggest checking them out when the videos are made public.</p>
<ul>
<li><a href='https://rubykaigi.org/2023/presentations/shioyama.html#day2' title=''>Multiverse Ruby by Chris Salzberg</a> brought up some interesting issues with global namespaces. His talk was in Japanese this year, which was really impressive
</li><li><a href='https://rubykaigi.org/2023/presentations/ioquatix.html#day3' title=''>Unleashing the Power of Asynchronous HTTP with Ruby by Samuel Williams</a> was really easy to follow and was great history lesson about the evolution of HTTP
</li><li><a href='https://rubykaigi.org/2023/presentations/saramic.html#day3' title=''>Ruby vs Kickboxer - the state of MRuby, JRuby and CRuby by Michael Milewski and Selena Small</a>. Sometimes all you need is <a href='https://twitter.com/hasumikin/status/1657242780559892482?s=20' title=''>a kickboxing robot</a>
</li><li><a href='https://rubykaigi.org/2023/presentations/KnuX.html#day1' title=''>Understanding the Ruby Global VM Lock by observing it by Ivo Anjo</a> pairs really well with <a href='https://rubykaigi.org/2023/presentations/ko1.html#day1' title=''>“Ractor” reconsidered by Koichi Sasada</a> if you’re interested in threading
</li><li>Special thanks to the organizers and volunteers of the conference!
</li></ul>
<h2 id='see-you-next-year-rubykaigi-2024' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#see-you-next-year-rubykaigi-2024' aria-label='Anchor'></a>See you next year RubyKaigi 2024! 🌴</h2>
<p>RubyKaigi 2024 is in Okinawa, the southern tropical islands of Japan. It’s going to be a blast. See you all there!</p>
/ruby-dispatch/ruby-kaigi-2023/assets/ruby-kaigi-2023.pngWelcome to Rails Cheat Sheethttps://fly.io/ruby-dispatch/welcome-to-rails-cheat-sheet/2023-05-01T00:00:00+00:002023-11-21T21:08:38+00:00<p><strong class='font-[550] text-navy-950'>Rails has been around since early 2004. Back then, the choices of building web applications was either working with a bunch of spaghetti code that was SFTP'ed up to a server or work with an enterprise monstrosity like Enterprise Java Beans. Rails showed us that web development could be fun <em>and</em> sane with built-in features like environments, database migrations, and a Model-View-Controller approach to building web applications.</strong></p>
<p>Let’s do some math. The year at the time of this writing is 2023. Rails was created on August of 2004. Rails is over <em>18 years old</em>! And to this day, after all this time, you’ll find passionate long-time Rails developers who swear by the framework as being the most productive for building and shipping applications, but many have taken for granted what it’s like to get started in Rails today and the plethora of other frameworks that have caught up.</p>
<h2 id='why-should-you-consider-rails' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#why-should-you-consider-rails' aria-label='Anchor'></a>Why should you consider Rails?</h2>
<p>Rails (and Ruby) cares a <em>lot</em> about developer happiness and productivity. It optimizes for the great feeling of, “<em>wow</em>, I got a lot done today”. You start by building a web application, which can be integrated into an Android & iOS application (more on that later).</p>
<p>Reading the <a href='https://rubyonrails.org/doctrine' title=''>Rails Doctrine</a> is a great place to get your bearings to put into context what the Rails community aspires to be. Despite those aspirations, you’ll sometimes find yourself in very disorienting places in Rails, like the <a href='https://fly.io/ruby-dispatch/making-sense-of-rails-assets/' title=''>asset pipeline</a>. Overall though, Rails is quite an enjoyable developer experience.</p>
<h2 id='getting-started' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#getting-started' aria-label='Anchor'></a>Getting Started</h2>
<p>If you’re getting started for the very first time with Rails you’ll want to have these resources open and ready in your browser as you build a Rails application.</p>
<h3 id='rails-guides' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#rails-guides' aria-label='Anchor'></a><a href='https://guides.rubyonrails.org/getting_started.html' title=''>Rails Guides</a></h3>
<p>Rails Guides are a great place to start reading about the basics of building a Rails application. Even when you’ve moved beyond the basics, you’ll still find yourself landing on guides to understand how a specific Rails library works.</p>
<h3 id='rails-api' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#rails-api' aria-label='Anchor'></a><a href='https://api.rubyonrails.org' title=''>Rails API</a></h3>
<p>When your journey moves past tutorials, you’ll find yourself referring to the API docs to see how specific classes or methods work in Rails. Its a reference that you’ll want to keep handy.</p>
<h3 id='ruby-docs' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#ruby-docs' aria-label='Anchor'></a><a href='https://www.ruby-lang.org/en/documentation/' title=''>Ruby Docs</a></h3>
<p>Just like the Rails API, there’s loads of stuff that you’ll be using from the Ruby language and its standard runtime. Sometimes it’s hard to know what’s in Ruby vs what’s in Rails, especially because of a huge library in Rails that extends the Ruby language called Active Support.</p>
<h3 id='chat-gpt' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#chat-gpt' aria-label='Anchor'></a><a href='https://fly.io/ruby-dispatch/pairing-with-gpt-4/' title=''>Chat GPT</a></h3>
<p>As large language models, like OpenAI’s Chat GPT-4 and Google’s Bard, become ever more capable, they also becoming better tools to help answer questions from people who are starting out in any new programming language. GPT-4 is a great resource for getting help, but <a href='https://fly.io/ruby-dispatch/pairing-with-gpt-4/' title=''>be warned</a> that its perfectly happy giving answers that seem plausible, but are either completely wrong, convoluted, or not a best practice.</p>
<h2 id='stuff-youll-inevitably-bump-into-with-rails' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#stuff-youll-inevitably-bump-into-with-rails' aria-label='Anchor'></a>Stuff you’ll inevitably bump into with Rails</h2>
<p>It might not say it in the beginners guides, but most people who ship Rails applications are using these technologies in their stack.</p>
<h3 id='postgres' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#postgres' aria-label='Anchor'></a><a href='http://www.postgresql.org' title=''>Postgres</a></h3>
<p><code>rails new</code> defaults to sqlite, which is a really great database, but when it’s time to deploy to production, most people choose Postgres as their database.</p>
<h3 id='redis' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#redis' aria-label='Anchor'></a><a href='https://redis.io' title=''>Redis</a></h3>
<p>I joke, “it really should be called Rails on Redis”, which is kind of true. Redis can be used in Rails for caches, background job processing, and WebSockets, ActionCable, and Hotwired.</p>
<p>Redis is really a Swiss Army knife. It’s great for caches because it’s really fast to read and write data to Redis. It also ships with pub/sub features that make it suitable for use by background workers and pushing notifications to browsers via WebSockets.</p>
<h3 id='sidekiq' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#sidekiq' aria-label='Anchor'></a><a href='https://sidekiq.org' title=''>Sidekiq</a></h3>
<p>Web requests should be fast, but sometimes your application needs to do something that takes a few minutes, like change the resolution of a large video file. The last thing you want in a web application is have a web server take a few minutes to fulfill a request—doing so would probably bring your web application to a crawl as requests stack up in a queue waiting for the server to respond.</p>
<p>The solution in Rails is to offload this request to a background worker, which frees up the web server to continue taking requests from other clients.</p>
<p>Sidekiq is the most popular background worker framework. You can’t go wrong using it since its battle tested in production by many large Rails applications.</p>
<h3 id='hotwire' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#hotwire' aria-label='Anchor'></a><a href='https://hotwired.dev' title=''>Hotwire</a></h3>
<p>Hotwire is part of Rails, but if you <a href='https://hotwired.dev/' title=''>landed on their website</a> you’d only see Rails mentioned as a footnote. Inside Rails, you’ll see these referred to as <a href='https://github.com/hotwired/turbo-rails' title=''>Turbo</a> and <a href='https://github.com/hotwired/stimulus-rails' title=''>Stimulus</a>.</p>
<p>Hotwire was created by the same people who created Rails in an effort to drastically reduce the amount of JavaScript needed to be written and deployed to the client. This approach to building low latency interactive web applications has surged in popularity over the past few years to combat the increasing complexity and frustrations Rails developers experienced when building JS web applications.</p>
<p>The framework is included by default when you create a new Rails application.</p>
<h3 id='ruby-gems-amp-bundler' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#ruby-gems-amp-bundler' aria-label='Anchor'></a><a href='https://rubygems.org' title=''>Ruby Gems</a> & <a href='https://bundler.io' title=''>Bundler</a></h3>
<p>At the root of your Rails application you’ll see a <code>Gemfile</code>. The entries in that file point to packages maintained by a community of Ruby developers that are hosted on <a href='https://rubygems.org' title=''>https://rubygems.org</a>.</p>
<h2 id='screencasts' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#screencasts' aria-label='Anchor'></a>Screencasts</h2>
<p>When you’re tired of reading documentation and simply want to sit on the couch and watch Rails, there’s a few notable screencasts worth watching.</p>
<h3 id='gorails' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#gorails' aria-label='Anchor'></a><a href='https://gorails.com/episodes?free=true' title=''>GoRails</a></h3>
<p>GoRails covers a wide variety of Rails & Ruby topics over a huge library of videos. Its really a great place for beginners who prefer to be lead step-by-step through a problem to learn.</p>
<h3 id='railscasts' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#railscasts' aria-label='Anchor'></a><a href='http://railscasts.com' title=''>RailsCasts</a></h3>
<p>A classic for those who got started a decade ago and still surprisingly relevant today. The Pro videos were recently made free by the author because the videos are dated.</p>
<h2 id='extending-rails' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#extending-rails' aria-label='Anchor'></a>Extending Rails</h2>
<p>As you start adding more features to your application, you will begin looking at plugins that add features to Rails apps like authentication, payment, and more. Fortunately there’s some really great resources out there to help you figure out which gem is most suitable for your project.</p>
<h3 id='the-ruby-toolbox' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-ruby-toolbox' aria-label='Anchor'></a><a href='https://www.ruby-toolbox.com' title=''>The Ruby Toolbox</a></h3>
<p>When I do research to see what RubyGem I need to install in Rails to solve a problem, the first place I look as <a href='https://www.ruby-toolbox.com' title=''>https://www.ruby-toolbox.com</a> to see what other similar libraries exist. This helps me make a more informed decision about which library I should close.</p>
<h3 id='awesome-ruby' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#awesome-ruby' aria-label='Anchor'></a><a href='https://awesome-ruby.com' title=''>Awesome Ruby</a></h3>
<p>Awesome Ruby is a more brief list of the popular RubyGem’s that the Ruby and Rails community use in their applications.</p>
<h2 id='newsletters' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#newsletters' aria-label='Anchor'></a>Newsletters</h2>
<p>You don’t need to subscribe to all of them, but its worth picking one to read at least once a week so you can keep track of all the new stuff and changes that comes to Rails.</p>
<h3 id='ruby-weekly' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#ruby-weekly' aria-label='Anchor'></a><a href='https://rubyweekly.com' title=''>Ruby Weekly</a></h3>
<p>Includes general news & updates about Ruby & Rails, links to new tutorials & articles, and a section on code & tools that are new or have been updated.</p>
<h3 id='short-ruby-news' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#short-ruby-news' aria-label='Anchor'></a><a href='https://newsletter.shortruby.com' title=''>Short Ruby News</a></h3>
<p>A summary of Tweets & Toots from people working on interesting problems within the Rails and Ruby community.</p>
<h3 id='this-week-in-rails' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#this-week-in-rails' aria-label='Anchor'></a><a href='https://world.hey.com/this.week.in.rails' title=''>This Week in Rails</a></h3>
<p>A weekly newsletter from the Rails team that covers changes and new features that land in Rails. This news letter is more narrowly focused on changes to the Rails framework, so you’d miss out on a lot of other news about Ruby Gems, plugins, and interesting community tutorials if you only subscribe to this.</p>
<h2 id='mobile-apps' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#mobile-apps' aria-label='Anchor'></a>Mobile Apps</h2>
<p>If you get to a point where you want to ship a mobile application for Rails, there’s a whole stack for that. The team behind Rails has also released libraries that make it possible to easier integrate a Rails application with mobile apps.</p>
<h3 id='turbo-ios-amp-android' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#turbo-ios-amp-android' aria-label='Anchor'></a><a href='https://github.com/hotwired/turbo-ios' title=''>Turbo iOS</a> & <a href='https://github.com/hotwired/turbo-android' title=''>Android</a></h3>
<p>The team behind Rails ships a library that makes basic integration with Rails possible. There’s still a lot of gaps in the framework that you’ll need to fill in, like authentication.</p>
<h3 id='the-turbo-native-guy' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-turbo-native-guy' aria-label='Anchor'></a><a href='https://masilotti.com/articles/' title=''>The Turbo Native Guy</a></h3>
<p><a href='https://masilotti.com/' title=''>Joe Masilotti</a> has been building Turbo iOS applications for years and writes about them on his website. A lot of his content gives some pretty great answers to the gaps left open in the Turbo Native frameworks. Joe has even released his own starter kit called <a href='https://github.com/joemasilotti/TurboNavigator' title=''>Turbo Navigator</a> that fills in a lot of the gaps left open in the Turbo iOS project.</p>
<h2 id='sharp-edges' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#sharp-edges' aria-label='Anchor'></a>Sharp edges</h2>
<p>Like anything, Rails has sharp edges that are worth knowing about when you start using it.</p>
<h3 id='javascript-css-and-image-assets' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#javascript-css-and-image-assets' aria-label='Anchor'></a>JavaScript, CSS, and image assets</h3>
<p>I’ve written about the <a href='https://fly.io/ruby-dispatch/making-sense-of-rails-assets/' title=''>asset pipeline</a> being confusing. The default importmaps pipeline is reasonable for somebody shipping a new app, but the moment you need to use a JavaScript package the requires compilation — or a node package’s CSS files, images, or other assets, you’ll find that the path forward can become overwhelming.</p>
<h3 id='deployments' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#deployments' aria-label='Anchor'></a>Deployments</h3>
<p>There’s no one way to deploy a Rails application, which is a decision that a person new to Rails has to make. Fortunately the upcoming Rails 7.1 release will include <a href='https://fly.io/ruby-dispatch/rails-on-docker/' title=''>a way to generate Dockerfiles</a>, which provide a standard configuration of the server you’ll deploy to production.</p>
<p>Better yet, the <code>dockerfile-rails</code> gem can be added to any Rails project to automatically generate Dockerfiles that can be deployed to host like Fly.io that accept Dockerfiles. Here’s how you can quickly generate a Dockerfile in your Rails app and deploy to production.</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-wogst5dj"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-wogst5dj"><span class="nv">$ </span>bundle add dockerfile-rails
<span class="nv">$ </span>bin/rails g dockerfile
<span class="nv">$ </span>brew add flyctl
<span class="nv">$ </span>flyctl launch
<span class="nv">$ </span>fly open
</code></pre>
</div>
</div>
<p>If that all goes well you’ll see a running Rails app.</p>
<h3 id='hotwire-2' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#hotwire-2' aria-label='Anchor'></a>Hotwire</h3>
<p>Hotwire makes it possible to build responsive & realtime web applications without a ton of JavaScript. The documentation at <a href='https://hotwired.dev/' title=''>https://hotwired.dev/</a> was written more for the front-end so it could be agnostic to Rails, which makes it harder to follow at times.</p>
<p>Rails ships with some helpers, called Turbo Frames and Turbo Stream, that are mostly documented in the Github repo at <a href='https://github.com/hotwired/turbo-rails' title=''>https://github.com/hotwired/turbo-rails</a>, but not yet in the Rails guides.</p>
<h3 id='quot-too-much-magic-quot' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#quot-too-much-magic-quot' aria-label='Anchor'></a>“Too much magic”</h3>
<p>If you fired up a Rails console via <code>bin/console rails</code> you’d find the following commands work as you’d expect</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-l3rb5ujp"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-l3rb5ujp">irb(main):001:0> 24.megabytes
=> 25165824
irb(main):002:0> 1.month.from_now
=> Fri, 07 Apr 2023 00:45:35.762342000 UTC +00:00
</code></pre>
</div>
</div>
<p>But if you ran the same thing in “Regular Ruby” via <code>irb</code> none of them would work. That’s because Rails adds methods to core Ruby classes that gives them super powers called <a href='https://guides.rubyonrails.org/active_support_core_extensions.html' title=''>Active Support</a>. You’ll find yourself switching between these docs and Ruby’s docs.</p>
<h2 id='this-is-the-tip-of-the-iceberg' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#this-is-the-tip-of-the-iceberg' aria-label='Anchor'></a>This is the tip of the iceberg</h2>
<p>There’s so many amazing people in the Ruby and Rails community shipping incredible stuff. What’s really great about the Rails community is how many resources are available for everybody using Rails including folks just starting out all the way up to the most Sr Rails engineers.</p>
<p>The best thing you can do to get started is pick a problem you’d like to solve, then build a solution for it in Rails and see how far you can get with the resources above.</p>
/ruby-dispatch/welcome-to-rails-cheat-sheet/assets/beginning-rails.pngHacking Rails Implicit Rendering for View Components & Funhttps://fly.io/ruby-dispatch/hacking-rails-implicit-rendering-for-view-components/2023-04-25T00:00:00+00:002023-11-21T21:08:38+00:00<p><strong class='font-[550] text-navy-950'>Understanding how Rails handles requests from routes.rb to calling the action method on a controller makes it possible to build Rails plugins ranging from Hanami-like action classes to component-driven Rails development.</strong></p>
<p>Have you ever opened a Rails controller that looks like this and wondered how it knows to render the view <code>./app/views/show.html.erb</code>?</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-wr2pf51o"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-wr2pf51o"><span class="k">class</span> <span class="nc">PostsController</span>
<span class="n">before_action</span> <span class="ss">:load_post</span>
<span class="c1"># How does Rails know to render ./app/views/show.html.erb</span>
<span class="c1"># when I don't write the `#show` method in my controller?</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">load_post</span>
<span class="vi">@post</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Rails has a method called <code>method_for_action</code> that Rails calls to perform this magic. At first glance it doesn’t seem like much, but it turns out we can do some pretty nifty things with this method, like <a href='https://fly.io/ruby-dispatch/component-driven-development-on-rails-with-phlex/' title=''>build a Rails application entirely from components using nothing but Phlex classes</a>.</p>
<h2 id='replace-def-show-with-class-show' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#replace-def-show-with-class-show' aria-label='Anchor'></a>Replace <code>def show</code> with <code>class Show</code></h2>
<p>Paste the code snippet in the controller concerns directory at <code>./app/controller/concerns/phlexable.rb</code> and you’ll be able embed Phlex views in your application controllers with the name <code>Show</code>, <code>Edit</code>, etc. to handle requests from <code>show</code>, <code>edit</code>, etc. respectively.</p>
<p>Let’s have a look at the entirety of the code.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-u0duvajk"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-u0duvajk"><span class="k">module</span> <span class="nn">Phlexable</span>
<span class="kp">extend</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Concern</span>
<span class="n">class_methods</span> <span class="k">do</span>
<span class="c1"># Finds a class on the controller with the same name as the action. For example,</span>
<span class="c1"># `def index` would find the `Index` constant on the controller class to render</span>
<span class="c1"># for the action `index`.</span>
<span class="k">def</span> <span class="nf">phlex_action_class</span><span class="p">(</span><span class="n">action</span><span class="p">:)</span>
<span class="n">action_class</span> <span class="o">=</span> <span class="n">action</span><span class="p">.</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">camelcase</span>
<span class="nb">const_get</span> <span class="n">action_class</span> <span class="k">if</span> <span class="nb">const_defined?</span> <span class="n">action_class</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="kp">protected</span>
<span class="c1"># Assigns the instance variables that are set in the controller to setter method</span>
<span class="c1"># on Phlex. For example, if a controller defines @users and a Phlex class has</span>
<span class="c1"># `attr_writer :users`, `attr_accessor :user`, or `def users=`, it will be automatically</span>
<span class="c1"># set by this method.</span>
<span class="k">def</span> <span class="nf">assign_phlex_accessors</span><span class="p">(</span><span class="n">phlex_view</span><span class="p">)</span>
<span class="n">phlex_view</span><span class="p">.</span><span class="nf">tap</span> <span class="k">do</span> <span class="o">|</span><span class="n">view</span><span class="o">|</span>
<span class="n">view_assigns</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">variable</span><span class="p">,</span> <span class="n">value</span><span class="o">|</span>
<span class="n">attr_writer_name</span> <span class="o">=</span> <span class="s2">"</span><span class="si">#{</span><span class="n">variable</span><span class="si">}</span><span class="s2">="</span>
<span class="n">view</span><span class="p">.</span><span class="nf">send</span> <span class="n">attr_writer_name</span><span class="p">,</span> <span class="n">value</span> <span class="k">if</span> <span class="n">view</span><span class="p">.</span><span class="nf">respond_to?</span> <span class="n">attr_writer_name</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># Initializers a Phlex view based on the action name, then assigns `view_assigns`</span>
<span class="c1"># to the view.</span>
<span class="k">def</span> <span class="nf">phlex_action</span><span class="p">(</span><span class="n">action</span><span class="p">)</span>
<span class="n">assign_phlex_accessors</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">phlex_action_class</span><span class="p">(</span><span class="ss">action: </span><span class="n">action</span><span class="p">).</span><span class="nf">new</span>
<span class="k">end</span>
<span class="c1"># Phlex action for the current action.</span>
<span class="k">def</span> <span class="nf">phlex</span>
<span class="n">phlex_action</span><span class="p">(</span><span class="n">action_name</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># Try rendering with the regular Rails rendering methods; if those don't work</span>
<span class="c1"># then try finding the Phlex class that corresponds with the action_name. If that's</span>
<span class="c1"># found then tell Rails to call `default_phlex_render`.</span>
<span class="k">def</span> <span class="nf">method_for_action</span><span class="p">(</span><span class="n">action_name</span><span class="p">)</span>
<span class="k">super</span> <span class="o">||</span> <span class="k">if</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">phlex_action_class</span> <span class="ss">action: </span><span class="n">action_name</span>
<span class="s2">"default_phlex_render"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># Renders a Phlex view for the given action, if it's present.</span>
<span class="k">def</span> <span class="nf">default_phlex_render</span>
<span class="n">render</span> <span class="n">phlex</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>The more interesting bit in this concern is <code>method_for_action</code>, which is what drives Rails “implicit rendering”.</p>
<p>Let’s break it down.</p>
<figure class="post-cta">
<figcaption>
<h1>Fly.io ❤️ Rails</h1>
<p>Fly.io is a great way to run your Rails app close to your users. It’s really easy to get started. You can be running in minutes.</p>
<a class="button bg-white text-violet-500 hover:bg-opacity-75 hover:text-violet-700 transition-colors" href="https://fly.io/docs/rails/">
Deploy a Rails app today! <span class='opacity:50'>→</span>
</a>
</figcaption>
<div class="image-container">
<img src="/static/images/cta-cat.webp" srcset="/static/images/cta-cat@2x.webp 2x" alt="">
</div>
</figure>
<h2 id='actioncontroller-implicitrender-method_for_action' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#actioncontroller-implicitrender-method_for_action' aria-label='Anchor'></a><code>ActionController::ImplicitRender#method_for_action</code></h2>
<p>This is a method in Rails that Rails asks, “what method should I call to render the action given to me by the router?”. At first you might be thinking, “that’s easy! If I route to <code>posts#index</code> just call <code>index</code>! Yes, but have you ever wondered how Rails finds <code>./app/views/posts/index.erb</code> if you don’t define an index method? It does so through <code>method_for_action</code>.</p>
<p>For the Phlex component renderer, I call <code>super</code> first, which is Rails default rendering stack. If <code>super</code> returns nil, meaning no methods or view templates were found to render the requested action, I check if the Phlex class exists in my controller. If the class does exist, I return the string <code>"default_phlex_render"</code>, which Rails then calls to render the action.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-nxduk2m8"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-nxduk2m8"><span class="c1"># Try rendering with the regular Rails rendering methods; if those don't work</span>
<span class="c1"># then try finding the Phlex class that corresponds with the action_name. If that's</span>
<span class="c1"># found then tell Rails to call `default_phlex_render`.</span>
<span class="k">def</span> <span class="nf">method_for_action</span><span class="p">(</span><span class="n">action_name</span><span class="p">)</span>
<span class="k">super</span> <span class="o">||</span> <span class="k">if</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">phlex_action_class</span> <span class="ss">action: </span><span class="n">action_name</span>
<span class="s2">"default_phlex_render"</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div><h3 id='default_phlex_render' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#default_phlex_render' aria-label='Anchor'></a><code>default_phlex_render</code></h3>
<p><code>method_for_action</code> returned the <code>"default_phlex_render"</code> string, which Rails then calls via <code>self.send("default_phlex_render")</code> to create an instance of the Phlex view and pass it to Rails built-in <code>render</code> method.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-t2zwlrpy"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-t2zwlrpy"><span class="c1"># Renders a Phlex view for the given action, if it's present.</span>
<span class="k">def</span> <span class="nf">default_phlex_render</span>
<span class="n">render</span> <span class="n">phlex</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>The <code>phlex</code> method returns an instance of a Phlex class for the requested action.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-x5yyvseb"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-x5yyvseb"><span class="c1"># Initializers a Phlex view based on the action name, then assigns `view_assigns`</span>
<span class="c1"># to the view.</span>
<span class="k">def</span> <span class="nf">phlex_action</span><span class="p">(</span><span class="n">action</span><span class="p">)</span>
<span class="n">assign_phlex_accessors</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">phlex_action_class</span><span class="p">(</span><span class="ss">action: </span><span class="n">action</span><span class="p">).</span><span class="nf">new</span>
<span class="k">end</span>
<span class="c1"># Phlex action for the current action.</span>
<span class="k">def</span> <span class="nf">phlex</span>
<span class="n">phlex_action</span><span class="p">(</span><span class="n">action_name</span><span class="p">)</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p><code>action_name</code> is an Action Controller method that returns the action as resolved by the router. For the sake of example, let’s say we’re requesting <code>/blog/1/posts</code>—the Rails router might resolve that to the <code>PostsController</code> controller and the <code>index</code> action name. The <code>"index"</code> string gets passed into <code>phlox_action</code> method, which it turns into a class name <code>Index</code> to check if it exists at <code>PostsController::Index</code> via the <code>const_get</code> method.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-owrfepnm"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-owrfepnm"><span class="k">def</span> <span class="nf">phlex_action_class</span><span class="p">(</span><span class="n">action</span><span class="p">:)</span>
<span class="n">action_class</span> <span class="o">=</span> <span class="n">action</span><span class="p">.</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">camelcase</span>
<span class="nb">const_get</span> <span class="n">action_class</span> <span class="k">if</span> <span class="nb">const_defined?</span> <span class="n">action_class</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>If the class does exist on the controller, an instance of it is created and then we assign the instance variables from the controller into the Phlex view class <em>only</em> if it has a setter defined.</p>
<p>This method could be modified to also look for views in other class hierarchies, like <code>View::Posts::Index</code>, for example.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-bq5xgpsi"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-bq5xgpsi"><span class="k">def</span> <span class="nf">assign_phlex_accessors</span><span class="p">(</span><span class="n">phlex_view</span><span class="p">)</span>
<span class="n">phlex_view</span><span class="p">.</span><span class="nf">tap</span> <span class="k">do</span> <span class="o">|</span><span class="n">view</span><span class="o">|</span>
<span class="n">view_assigns</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">variable</span><span class="p">,</span> <span class="n">value</span><span class="o">|</span>
<span class="n">attr_writer_name</span> <span class="o">=</span> <span class="s2">"</span><span class="si">#{</span><span class="n">variable</span><span class="si">}</span><span class="s2">="</span>
<span class="n">view</span><span class="p">.</span><span class="nf">send</span> <span class="n">attr_writer_name</span><span class="p">,</span> <span class="n">value</span> <span class="k">if</span> <span class="n">view</span><span class="p">.</span><span class="nf">respond_to?</span> <span class="n">attr_writer_name</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Finally we copy the instance variables that were set in the controller into the class, but <em>only</em> if the Phlex view component has a setter method that matches the instance variable name. For example, <code>@posts</code> in the controller would set <code>Index#posts=</code>. Why not simply copy the instance variables from the controller into the Phlex view? Because it make the component leaky and harder to test since it breaks encapsulation. Think about it, would you rather setup a view component test like this?</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-7c44rndb"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-7c44rndb"><span class="c1"># Eww, nobody wants to initialize a view this way</span>
<span class="n">index</span> <span class="o">=</span> <span class="no">Index</span><span class="p">.</span><span class="nf">new</span>
<span class="n">index</span><span class="p">.</span><span class="nf">instance_variable_set</span><span class="p">(</span><span class="s2">"@blogs"</span><span class="p">,</span> <span class="no">Blog</span><span class="p">.</span><span class="nf">all</span><span class="p">)</span>
</code></pre>
</div>
</div>
<p>Or like this?</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-jtga6so"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-jtga6so"><span class="c1"># That's a little better</span>
<span class="n">index</span> <span class="o">=</span> <span class="no">Index</span><span class="p">.</span><span class="nf">new</span>
<span class="n">index</span><span class="p">.</span><span class="nf">blogs</span> <span class="o">=</span> <span class="no">Blog</span><span class="p">.</span><span class="nf">all</span>
</code></pre>
</div>
</div>
<p>The latter is easier to test and is preferred, but if for whatever reason you wanted to copy over all the instance variables directly into the class from the controller, now you know how to do it.</p>
<h2 id='taking-it-further' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#taking-it-further' aria-label='Anchor'></a>Taking it further</h2>
<p>In this example we used Phlex classes to render Rails views, but now that you know how to dispatch a request to a class embedded in a Rails controller, there’s lots that could be done including:</p>
<h3 id='viewcomponent-integration' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#viewcomponent-integration' aria-label='Anchor'></a>ViewComponent integration</h3>
<p>If the <a href='https://viewcomponent.org' title=''>ViewComponent</a> library is your cup of tea, the same technique used to render Phlex views could be applied to the ViewComponent gem.</p>
<h3 id='action-classes' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#action-classes' aria-label='Anchor'></a>Action classes</h3>
<p>If you have code that’s heavy on <code>before_action</code> and <code>after_action</code> calls and inheriting ActionControllers doesn’t make sense, it’s possible to build <a href='https://guides.hanamirb.org/v1.3/actions/overview/' title=''>Hanami-style action classes</a> that can be nested in an Action Controller. It’s not 100% the same thing, but maybe its an abstraction you need for your application.</p>
<h3 id='more-sophisticated-phlex-view-class-lookups' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#more-sophisticated-phlex-view-class-lookups' aria-label='Anchor'></a>More sophisticated Phlex view class lookups</h3>
<p>If you decide to build a Rails app entirely from Phlex components, you could look up constants in different name spaces like <code>Views::#{controller.name}::#{controller.action_name}</code>, or break it out into formats like <code>Views::#{controller.name}::#{request.format}::#{controller.action_name}</code>.</p>
<h3 id='format-classes' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#format-classes' aria-label='Anchor'></a>Format classes</h3>
<p>If you’re constantly implementing <code>format.html</code>, <code>format.json</code> blocks in your application, you could replace them with a responder class.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-4r0wdp31"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-4r0wdp31"><span class="k">class</span> <span class="nc">ApplicationResponder</span>
<span class="nb">attr_accessor</span> <span class="ss">:model</span><span class="p">,</span> <span class="ss">:controller</span>
<span class="n">delegate</span> <span class="ss">:action_name</span><span class="p">,</span> <span class="ss">:render</span><span class="p">,</span> <span class="ss">to: :controller</span>
<span class="k">def</span> <span class="nf">html</span>
<span class="n">render</span> <span class="n">action_name</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">json</span>
<span class="vi">@model</span><span class="p">.</span><span class="nf">as_json</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div><h3 id='batch-resource-manipulation' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#batch-resource-manipulation' aria-label='Anchor'></a>Batch resource manipulation</h3>
<p>Handling batches of resources in Rails in a secure manner can get awkward because Rails wants each action to have its own URL, but rendering an HTML form with a list of check boxes next to each item in a form results in a request payload that looks like this:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-sxqcnj7k"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-sxqcnj7k"><span class="p">{</span>
<span class="ss">batch: </span><span class="p">{</span>
<span class="ss">selected: </span><span class="p">[</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">4</span><span class="p">,</span><span class="mi">5</span><span class="p">,</span><span class="mi">6</span><span class="p">,</span><span class="mi">7</span><span class="p">],</span>
<span class="ss">action: </span><span class="s2">"delete
}
}
</span></code></pre>
</div>
</div>
<p><code>method_for_action</code> can match <code>params[:batch][:action]</code> to the correct action in our batch controllers and make sure the endpoints are publicly accessible routes with this little snippet of code:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-69dva406"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-69dva406"><span class="k">module</span> <span class="nn">Batchable</span>
<span class="kp">extend</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Concern</span>
<span class="kp">protected</span>
<span class="k">def</span> <span class="nf">method_for_action</span><span class="p">(</span><span class="n">action_name</span><span class="p">)</span>
<span class="n">routable_batch_action?</span> <span class="p">?</span> <span class="n">batch_action</span> <span class="p">:</span> <span class="k">super</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">batch_action</span>
<span class="n">params</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="s2">"batch"</span><span class="p">,</span> <span class="s2">"action"</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">routable_batch_action?</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">action_methods</span><span class="p">.</span><span class="nf">include?</span> <span class="n">batch_action</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Which we connect to our routes via this nifty little helper.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-3r8eiiwt"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-3r8eiiwt"><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
<span class="n">resources</span> <span class="ss">:blogs</span> <span class="k">do</span>
<span class="n">nest</span> <span class="ss">:posts</span> <span class="k">do</span>
<span class="n">batch</span> <span class="ss">:delete</span><span class="p">,</span> <span class="ss">:publish</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>There’s a more that goes into batch, which I’ll cover in a future post.</p>
/ruby-dispatch/hacking-rails-implicit-rendering-for-view-components/assets/cover.pngComponent driven development on Rails with Phlexhttps://fly.io/ruby-dispatch/component-driven-development-on-rails-with-phlex/2023-04-18T00:00:00+00:002023-11-21T21:08:38+00:00<p><strong class='font-[550] text-navy-950'>Building applications entirely from UI components can be a great way to manage complexity in non-trivial application views, especially when using CSS frameworks like TailwindCSS. It’s a technique that’s been used with great success, by many communities, like JavaScript and Elixir.</strong></p>
<p>What if you could rapidly build Rails applications like this?</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-32ivtovz"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-32ivtovz"><span class="k">class</span> <span class="nc">PostsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">resources</span> <span class="ss">:posts</span><span class="p">,</span> <span class="ss">from: :current_user</span>
<span class="k">class</span> <span class="nc">Form</span> <span class="o"><</span> <span class="no">ApplicationForm</span>
<span class="k">def</span> <span class="nf">template</span>
<span class="n">field</span> <span class="ss">:title</span>
<span class="n">field</span> <span class="ss">:publish_at</span>
<span class="n">field</span> <span class="ss">:content</span><span class="p">,</span> <span class="ss">rows: </span><span class="mi">6</span>
<span class="n">submit</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">New</span> <span class="o"><</span> <span class="no">ApplicationView</span>
<span class="nb">attr_accessor</span> <span class="ss">:current_user</span><span class="p">,</span> <span class="ss">:blog</span><span class="p">,</span> <span class="ss">:post</span>
<span class="k">def</span> <span class="nf">title</span> <span class="o">=</span> <span class="s2">"Create a new post"</span>
<span class="k">def</span> <span class="nf">subtitle</span> <span class="o">=</span> <span class="n">show</span><span class="p">(</span><span class="vi">@blog</span><span class="p">,</span> <span class="ss">:title</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">template</span><span class="p">(</span><span class="o">&</span><span class="p">)</span>
<span class="n">render</span> <span class="no">Form</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="vi">@post</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>That emit a HTML user interfaces like this?</p>
<p><img alt="Screenshot of a blog post generated by Phlex components" src="/images/blog-post.png" /></p>
<p>Try it yourself by <a href='https://oxidizer-demo.fly.dev' title=''>exploring the demo</a>, <a href='https://github.com/rubymonolith/demo' title=''>dig into the source code</a>, and continue reading for a tour of a Rails application built entirely from Phlex components.</p>
<p><a href='https://www.phlex.fun' title=''>Phlex</a>, a pure Ruby framework for building HTML components, and some integration code with Rails, opens up the possibility of building applications in Rails entirely from components.</p>
<p>Sure, there’s Rails scaffolding and other ways of generating code, but generating lots of scaffolding views puts files in your project that <em>all</em> have to be changed when its time to switch from Rails scaffolding markup to whatever gets deployed to production. How good are you at grep?</p>
<p>With Phlex components, iterating on the UI becomes an exercise of extending and refining Ruby classes, so there’s a lot less files to deal with meaning you can ship a higher quality product to production much faster.</p>
<h2 id='a-tour-of-a-working-rails-application-built-entirely-from-phlex-components' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#a-tour-of-a-working-rails-application-built-entirely-from-phlex-components' aria-label='Anchor'></a>A tour of a working Rails application built entirely from Phlex components</h2>
<p>First it’s important to understand the basics of Phlex. A basic HTML component has an initializer and a <code>template</code> method. The temple method is what’s called when the component is rendered. Within the template method you’ll notice blocks that generate HTML tags, in this case <code><p/></code> and <code><h1/></code>.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-rtyvkwk"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-rtyvkwk"><span class="k">class</span> <span class="nc">BasicComponent</span> <span class="o"><</span> <span class="no">Phlex</span><span class="o">::</span><span class="no">HTML</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="nb">name</span><span class="p">:)</span>
<span class="vi">@name</span> <span class="o">=</span> <span class="nb">name</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">template</span><span class="p">(</span><span class="o">&</span><span class="p">)</span>
<span class="n">h1</span> <span class="p">{</span> <span class="s2">"Hello </span><span class="si">#{</span><span class="vi">@name</span><span class="p">.</span><span class="nf">capitalize</span><span class="si">}</span><span class="s2">"</span> <span class="p">}</span>
<span class="nb">p</span> <span class="p">{</span> <span class="s2">"I hope you're doing incredibly well!"</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>To render it in Rails, we call <code>ActionController#render</code>.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-ofwm4pwg"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-ofwm4pwg"><span class="n">render</span> <span class="no">BasicComponent</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Brad"</span><span class="p">)</span>
</code></pre>
</div>
</div>
<p>That’s it for the basics! It might not seem like much, but this simplicity and consistency is what makes it possible to compose complex HTML views in Rails that are easier to reason through than a bunch of templates, partials, and helper methods.</p>
<p>Now buckle up, we’re going to continue our tour with <a href='https://github.com/rubymonolith/demo' title=''>a demo RESTful Blog</a> app that answers the question, “what happens if I build a Rails app entirely out of Phlex components?”</p>
<figure class="post-cta">
<figcaption>
<h1>Fly.io ❤️ Rails</h1>
<p>Fly.io is a great way to run your Rails app close to your users. It’s really easy to get started. You can be running in minutes.</p>
<a class="button bg-white text-violet-500 hover:bg-opacity-75 hover:text-violet-700 transition-colors" href="https://fly.io/docs/rails/">
Deploy a Rails app today! <span class='opacity:50'>→</span>
</a>
</figcaption>
<div class="image-container">
<img src="/static/images/cta-dog.webp" srcset="/static/images/cta-dog@2x.webp 2x" alt="">
</div>
</figure>
<h2 id='views-in-controllers' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#views-in-controllers' aria-label='Anchor'></a>Views in controllers</h2>
<p>There’s <a href='https://github.com/rubymonolith/demo/blob/main/app/controllers/concerns/phlexable.rb' title=''>some magic</a> that I wrote about in <a href='https://fly.io/ruby-dispatch/hacking-rails-implicit-rendering-for-view-components/' title=''>Hacking Rails Implicit Rendering for View Components & Fun</a> on how I map class names to Rails action names, but to keep this tour running on-time, just know that the <code>show</code> action for a resource maps to the <code>Show</code> class, <code>edit</code> to <code>Edit</code>, and so on.</p>
<p>This is what the <code>PostsController</code> looks like with inline Phlex views:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-5q9708nu"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-5q9708nu"><span class="k">class</span> <span class="nc">PostsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">resources</span> <span class="ss">:posts</span><span class="p">,</span> <span class="ss">from: :current_user</span>
<span class="k">class</span> <span class="nc">Form</span> <span class="o"><</span> <span class="no">ApplicationForm</span>
<span class="k">def</span> <span class="nf">template</span>
<span class="n">field</span> <span class="ss">:title</span>
<span class="n">field</span> <span class="ss">:publish_at</span>
<span class="n">field</span> <span class="ss">:content</span><span class="p">,</span> <span class="ss">rows: </span><span class="mi">6</span>
<span class="n">submit</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">Index</span> <span class="o"><</span> <span class="no">ApplicationView</span>
<span class="nb">attr_writer</span> <span class="ss">:posts</span><span class="p">,</span> <span class="ss">:current_user</span>
<span class="k">def</span> <span class="nf">title</span> <span class="o">=</span> <span class="s2">"</span><span class="si">#{</span><span class="vi">@current_user</span><span class="p">.</span><span class="nf">name</span><span class="si">}</span><span class="s2">'s Posts"</span>
<span class="k">def</span> <span class="nf">template</span>
<span class="n">render</span> <span class="no">TableComponent</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">items: </span><span class="vi">@posts</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">table</span><span class="o">|</span>
<span class="n">table</span><span class="p">.</span><span class="nf">column</span><span class="p">(</span><span class="s2">"Title"</span><span class="p">)</span> <span class="p">{</span> <span class="n">show</span><span class="p">(</span><span class="n">_1</span><span class="p">,</span> <span class="ss">:title</span><span class="p">)</span> <span class="p">}</span>
<span class="n">table</span><span class="p">.</span><span class="nf">column</span> <span class="k">do</span> <span class="o">|</span><span class="n">column</span><span class="o">|</span>
<span class="c1"># Titles might not always be text, so we need to handle rendering</span>
<span class="c1"># Phlex markup within.</span>
<span class="n">column</span><span class="p">.</span><span class="nf">title</span> <span class="k">do</span>
<span class="n">link_to</span><span class="p">(</span><span class="n">user_blogs_path</span><span class="p">(</span><span class="vi">@current_user</span><span class="p">))</span> <span class="p">{</span> <span class="s2">"Blogs"</span> <span class="p">}</span>
<span class="k">end</span>
<span class="n">column</span><span class="p">.</span><span class="nf">item</span> <span class="p">{</span> <span class="n">show</span><span class="p">(</span><span class="n">_1</span><span class="p">.</span><span class="nf">blog</span><span class="p">,</span> <span class="ss">:title</span><span class="p">)</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="k">class</span> <span class="nc">Show</span> <span class="o"><</span> <span class="no">ApplicationView</span>
<span class="nb">attr_writer</span> <span class="ss">:post</span>
<span class="k">def</span> <span class="nf">title</span> <span class="o">=</span> <span class="vi">@post</span><span class="p">.</span><span class="nf">title</span>
<span class="k">def</span> <span class="nf">subtitle</span> <span class="o">=</span> <span class="n">show</span><span class="p">(</span><span class="vi">@post</span><span class="p">.</span><span class="nf">blog</span><span class="p">,</span> <span class="ss">:title</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">template</span>
<span class="n">table</span> <span class="k">do</span>
<span class="n">tbody</span> <span class="k">do</span>
<span class="n">tr</span> <span class="k">do</span>
<span class="n">th</span> <span class="p">{</span> <span class="s2">"Publish at"</span> <span class="p">}</span>
<span class="n">td</span> <span class="p">{</span> <span class="vi">@post</span><span class="p">.</span><span class="nf">publish_at</span><span class="o">&</span><span class="p">.</span><span class="nf">to_formatted_s</span><span class="p">(</span><span class="ss">:long</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="n">tr</span> <span class="k">do</span>
<span class="n">th</span> <span class="p">{</span> <span class="s2">"Status"</span> <span class="p">}</span>
<span class="n">td</span> <span class="p">{</span> <span class="vi">@post</span><span class="p">.</span><span class="nf">status</span> <span class="p">}</span>
<span class="k">end</span>
<span class="n">tr</span> <span class="k">do</span>
<span class="n">th</span> <span class="p">{</span> <span class="s2">"Content"</span> <span class="p">}</span>
<span class="n">td</span> <span class="k">do</span>
<span class="n">article</span> <span class="p">{</span> <span class="vi">@post</span><span class="p">.</span><span class="nf">content</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">nav</span> <span class="k">do</span>
<span class="n">edit</span><span class="p">(</span><span class="vi">@post</span><span class="p">,</span> <span class="ss">role: </span><span class="s2">"button"</span><span class="p">)</span>
<span class="n">delete</span><span class="p">(</span><span class="vi">@post</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">Edit</span> <span class="o"><</span> <span class="no">ApplicationView</span>
<span class="nb">attr_writer</span> <span class="ss">:post</span>
<span class="k">def</span> <span class="nf">title</span> <span class="o">=</span> <span class="vi">@post</span><span class="p">.</span><span class="nf">title</span>
<span class="k">def</span> <span class="nf">subtitle</span> <span class="o">=</span> <span class="n">show</span><span class="p">(</span><span class="vi">@post</span><span class="p">.</span><span class="nf">blog</span><span class="p">,</span> <span class="ss">:title</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">template</span>
<span class="n">render</span> <span class="no">Form</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="vi">@post</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">destroyed_url</span>
<span class="vi">@post</span><span class="p">.</span><span class="nf">blog</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div><h3 id='why-inline-views' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#why-inline-views' aria-label='Anchor'></a>Why inline views?</h3>
<p>I’ve found over the years that I really enjoy prototyping my applications in frameworks like Sinatra where views, controllers, and routing are all closer together; however, when the application grows complex, I find myself wishing I’d built it in Rails.</p>
<p>With inline Phlex views in my controller, I can have the best of both worlds—build out views in the controller, then when it’s time to re-use them somewhere else, I can move the views into their own files.</p>
<h3 id='extract-inline-views-into-view-files' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#extract-inline-views-into-view-files' aria-label='Anchor'></a>Extract inline views into view files</h3>
<p>For example, I could move the <code>PostsController::Edit</code> view into its own file at <code>./app/views/posts/edit.rb</code>, namespace it to <code>Posts::Edit</code>,</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-77t25i4a"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-77t25i4a"><span class="k">class</span> <span class="nc">Posts::Edit</span> <span class="o"><</span> <span class="no">ApplicationView</span>
<span class="nb">attr_writer</span> <span class="ss">:post</span>
<span class="k">def</span> <span class="nf">title</span> <span class="o">=</span> <span class="vi">@post</span><span class="p">.</span><span class="nf">title</span>
<span class="k">def</span> <span class="nf">subtitle</span> <span class="o">=</span> <span class="n">show</span><span class="p">(</span><span class="vi">@post</span><span class="p">.</span><span class="nf">blog</span><span class="p">,</span> <span class="ss">:title</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">template</span>
<span class="n">render</span> <span class="no">Form</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="vi">@post</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Then change the call from the <code>PostsController</code> to what you’d expect:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-hqztj0m3"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-hqztj0m3"><span class="k">class</span> <span class="nc">PostsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">edit</span>
<span class="n">render</span> <span class="no">Posts</span><span class="o">::</span><span class="no">Edit</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">post: </span><span class="vi">@post</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>In practice though, I’m finding I don’t really need to do this if I’m heavily refactoring view classes to be highly compentized since the views end up being pretty small.</p>
<h3 id='can-my-views-co-exist-with-my-existing-erb-templates' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#can-my-views-co-exist-with-my-existing-erb-templates' aria-label='Anchor'></a>Can my views co-exist with my existing Erb templates?</h3>
<p>Yep! That’s what’s magical about this approach, you can have classes, explicit rendering, and implicit rendering in the same controller.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-znt3lg8k"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-znt3lg8k"><span class="k">class</span> <span class="nc">PostsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="c1"># Your fancy new Edit code</span>
<span class="k">class</span> <span class="nc">Edit</span> <span class="o"><</span> <span class="no">ApplicationView</span>
<span class="nb">attr_writer</span> <span class="ss">:post</span>
<span class="k">def</span> <span class="nf">title</span> <span class="o">=</span> <span class="vi">@post</span><span class="p">.</span><span class="nf">title</span>
<span class="k">def</span> <span class="nf">subtitle</span> <span class="o">=</span> <span class="n">show</span><span class="p">(</span><span class="vi">@post</span><span class="p">.</span><span class="nf">blog</span><span class="p">,</span> <span class="ss">:title</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">template</span>
<span class="n">render</span> <span class="no">Form</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="vi">@post</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># Your existing Erb code</span>
<span class="k">def</span> <span class="nf">show</span>
<span class="n">respond_to</span> <span class="k">do</span> <span class="o">|</span><span class="nb">format</span><span class="o">|</span>
<span class="nb">format</span><span class="p">.</span><span class="nf">html</span> <span class="p">{</span> <span class="n">render</span> <span class="s2">"blog/posts/show"</span> <span class="p">}</span>
<span class="nb">format</span><span class="p">.</span><span class="nf">txt</span> <span class="p">{</span> <span class="n">render</span> <span class="ss">plain: </span><span class="vi">@post</span><span class="p">.</span><span class="nf">to_s</span> <span class="p">}</span>
<span class="nb">format</span><span class="p">.</span><span class="nf">json</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>That means you could retrofit existing Rails applications with inline class views, meaning if you wanted to convert your application entirely to components, you could do so action-by-action.</p>
<h2 id='application-layout-component' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#application-layout-component' aria-label='Anchor'></a>Application layout component</h2>
<p>Phlex’s application layout emits the same markup as Rails’ default application layout file.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-8w9m3elh"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-8w9m3elh"><span class="k">class</span> <span class="nc">ApplicationLayout</span> <span class="o"><</span> <span class="no">ApplicationComponent</span>
<span class="kp">include</span> <span class="no">Phlex</span><span class="o">::</span><span class="no">Rails</span><span class="o">::</span><span class="no">Layout</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">title</span><span class="p">:)</span>
<span class="vi">@title</span> <span class="o">=</span> <span class="n">title</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">template</span><span class="p">(</span><span class="o">&</span><span class="p">)</span>
<span class="n">doctype</span>
<span class="n">html</span> <span class="k">do</span>
<span class="n">head</span> <span class="k">do</span>
<span class="n">title</span><span class="p">(</span><span class="o">&</span><span class="vi">@title</span><span class="p">)</span>
<span class="n">meta</span> <span class="ss">name: </span><span class="s2">"viewport"</span><span class="p">,</span> <span class="ss">content: </span><span class="s2">"width=device-width,initial-scale=1"</span>
<span class="n">csp_meta_tag</span>
<span class="n">csrf_meta_tags</span>
<span class="n">stylesheet_link_tag</span> <span class="s2">"application"</span><span class="p">,</span> <span class="ss">data_turbo_track: </span><span class="s2">"reload"</span>
<span class="n">javascript_importmap_tags</span>
<span class="k">end</span>
<span class="n">body</span><span class="p">(</span><span class="o">&</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>We hook the <code>ApplicationLayout</code> into the <code>ApplicationView</code> with the <code>around_template</code> callback that Phlex gives us to wrap any of our views with layouts.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-pod8srm5"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-pod8srm5"><span class="k">class</span> <span class="nc">ApplicationView</span> <span class="o"><</span> <span class="no">ApplicationComponent</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">title</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="k">def</span> <span class="nf">subtitle</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="k">def</span> <span class="nf">around_template</span><span class="p">(</span><span class="o">&</span><span class="p">)</span>
<span class="n">render</span> <span class="no">PageLayout</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">title: </span><span class="nb">proc</span> <span class="p">{</span> <span class="n">title</span> <span class="p">},</span> <span class="ss">subtitle: </span><span class="nb">proc</span> <span class="p">{</span> <span class="n">subtitle</span> <span class="p">})</span> <span class="k">do</span>
<span class="k">super</span><span class="p">(</span><span class="o">&</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
</code></pre>
</div>
</div><h3 id='how-quot-slots-quot-work' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#how-quot-slots-quot-work' aria-label='Anchor'></a>How “slots” work</h3>
<p>There’s a few interesting things going on here. You’ll notice the <code>title</code> and <code>subtitle</code> methods are set to nil in the <code>ApplicationView</code> class. You’ll also notice we call these methods from within a proc, and we pass the proc to the <code>PageLayout</code> class.</p>
<p>The <code>PageLayout</code> class calls the <code>h1</code> method and <code>h2</code> method respectively with the <code>@subtitle</code> and <code>@title</code> blocks. These blocks are the conceptual equivalent of custom HTML element “slots”.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-grl00von"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-grl00von"><span class="k">class</span> <span class="nc">PageLayout</span> <span class="o"><</span> <span class="no">ApplicationLayout</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="ss">title: </span><span class="kp">nil</span><span class="p">,</span> <span class="ss">subtitle: </span><span class="kp">nil</span><span class="p">)</span>
<span class="vi">@title</span> <span class="o">=</span> <span class="n">title</span>
<span class="vi">@subtitle</span> <span class="o">=</span> <span class="n">subtitle</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">template</span><span class="p">(</span><span class="o">&</span><span class="p">)</span>
<span class="k">super</span> <span class="k">do</span>
<span class="n">header</span><span class="p">(</span><span class="ss">class: </span><span class="s2">"container"</span><span class="p">)</span> <span class="k">do</span>
<span class="k">if</span> <span class="vi">@title</span> <span class="n">and</span> <span class="vi">@subtitle</span>
<span class="n">hgroup</span> <span class="k">do</span>
<span class="n">h1</span><span class="p">(</span><span class="o">&</span><span class="vi">@title</span><span class="p">)</span>
<span class="n">h2</span><span class="p">(</span><span class="o">&</span><span class="vi">@subtitle</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">else</span>
<span class="n">h1</span> <span class="p">{</span> <span class="vi">@title</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">main</span><span class="p">(</span><span class="ss">class: </span><span class="s2">"container"</span><span class="p">,</span> <span class="o">&</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>The <code>super</code> method in <code>template</code> calls <code>ApplicationLayout#template</code>, which calls the <code>title(&@title)</code> tag inside <code>head</code> to set the title of the HTML document.</p>
<p>Let’s put that all together with a <code>New</code> view that displays a form to create a blog post.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-opahrewj"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-opahrewj"><span class="k">class</span> <span class="nc">PostsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="c1"># ...</span>
<span class="k">class</span> <span class="nc">New</span> <span class="o"><</span> <span class="no">ApplicationView</span>
<span class="nb">attr_accessor</span> <span class="ss">:current_user</span><span class="p">,</span> <span class="ss">:blog</span><span class="p">,</span> <span class="ss">:post</span>
<span class="k">def</span> <span class="nf">title</span> <span class="o">=</span> <span class="s2">"Create a new post"</span>
<span class="k">def</span> <span class="nf">subtitle</span> <span class="o">=</span> <span class="n">show</span><span class="p">(</span><span class="vi">@blog</span><span class="p">,</span> <span class="ss">:title</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">template</span><span class="p">(</span><span class="o">&</span><span class="p">)</span>
<span class="n">render</span> <span class="no">Form</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="vi">@post</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>When we request <code>/blogs/:id/posts/new</code>, we see our layout that shows the <code><title/></code> and <code><h1/></code> tags set to <code>@title</code> and the <code><h2/></code> tag set to <code>@subtitle</code>.</p>
<p><img alt="Screenshot of a blog post generated by Phlex components" src="/images/blog-post.png" /></p>
<p>In the world of custom HTML template, we’d call these blocks “slots”. Slots are how blocks of markup can be passed into layouts, components, etc.</p>
<h2 id='a-form-component-that-automatically-permits-strong-parameters' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#a-form-component-that-automatically-permits-strong-parameters' aria-label='Anchor'></a>A form component that automatically permits strong parameters</h2>
<p>One of the most aggravating experiences for me as a Rails developer is when I build a form, add a field, test it out in my browser and wonder why it’s not persisting to the database. 99% of the time its because I forgot to permit the Action Controller param. Arg!</p>
<p>I’m so lazy that I’ll spend 20 hours to save 10 minutes of work, so I built a Phlex form component that tracks which form fields I’m rendering and passes them to the controller to permit the attributes.</p>
<p>Consider the blog post form with the <code>:title</code>, <code>:publish_at</code>, and <code>:content</code> fields.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-rfbjhuzh"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-rfbjhuzh"><span class="k">class</span> <span class="nc">PostsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">resources</span> <span class="ss">:posts</span><span class="p">,</span> <span class="ss">from: :current_user</span>
<span class="k">class</span> <span class="nc">Form</span> <span class="o"><</span> <span class="no">ApplicationForm</span>
<span class="k">def</span> <span class="nf">template</span>
<span class="n">field</span> <span class="ss">:title</span>
<span class="n">field</span> <span class="ss">:publish_at</span>
<span class="n">field</span> <span class="ss">:content</span><span class="p">,</span> <span class="ss">rows: </span><span class="mi">6</span>
<span class="n">submit</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>If you <a href='https://github.com/rubymonolith/demo/blob/cfc1ef83ee19816ffab1d62c11e6b95c0d589fc1/app/views/phlex_form.rb#L39' title=''>dig deep enough into the form abstraction</a>, which is still a work in progress, you’ll see code that looks like this:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-ed7liplp"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-ed7liplp"><span class="k">def</span> <span class="nf">input_field</span><span class="p">(</span><span class="n">field</span><span class="p">:,</span> <span class="ss">value: </span><span class="kp">nil</span><span class="p">,</span> <span class="ss">type: </span><span class="kp">nil</span><span class="p">,</span> <span class="o">**</span><span class="n">attributes</span><span class="p">)</span>
<span class="vi">@fields</span> <span class="o"><<</span> <span class="n">field</span> <span class="c1"># This tracks the fields that the form uses</span>
<span class="n">input</span><span class="p">(</span>
<span class="ss">name: </span><span class="n">field_name</span><span class="p">(</span><span class="n">field</span><span class="p">),</span>
<span class="ss">type: </span><span class="n">type</span><span class="p">,</span>
<span class="ss">value: </span><span class="n">value</span> <span class="o">||</span> <span class="n">model_value</span><span class="p">(</span><span class="n">field</span><span class="p">),</span>
<span class="o">**</span><span class="n">attributes</span>
<span class="p">)</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Which gets called from the <a href='https://github.com//rubymonolith/demo/blob/cfc1ef83ee19816ffab1d62c11e6b95c0d589fc1/app/views/application_form.rb#L4-L20' title=''>higher level <code>field</code> method</a>:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-izje198k"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-izje198k"><span class="n">input_field</span><span class="p">(</span><span class="ss">:title</span><span class="p">,</span> <span class="ss">type: </span><span class="s2">"text"</span><span class="p">)</span>
</code></pre>
</div>
</div>
<p>The magic happens when I append the <code>:title</code> field name symbol, and all the other field names I call from my form, to the <code>@fields</code> array.</p>
<p>I have <a href='https://github.com//rubymonolith/demo/blob/cfc1ef83ee19816ffab1d62c11e6b95c0d589fc1/app/views/application_form.rb#L22-L24' title=''>another method</a> in the <code>ApplicationForm</code> that permits controller’s params.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-ufqbkd2r"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-ufqbkd2r"><span class="k">def</span> <span class="nf">permit</span><span class="p">(</span><span class="n">params</span><span class="p">)</span>
<span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="vi">@model</span><span class="p">.</span><span class="nf">model_name</span><span class="p">.</span><span class="nf">param_key</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span><span class="o">*</span><span class="vi">@fields</span><span class="p">)</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Finally, from my controller code I put it all together by creating an instance of the form, and passing the controllers params into the <code>permit</code> method to permit the keys.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-k8iyl0xr"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-k8iyl0xr"><span class="k">class</span> <span class="nc">PostsController</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">permitted_params</span>
<span class="no">Views</span><span class="o">::</span><span class="no">Posts</span><span class="o">::</span><span class="no">Form</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">permit</span><span class="p">(</span><span class="n">params</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>The form view component is capable of figuring out which fields I’ve permitted from the view and eliminates the problem of “forgetting to permit a param that you’ve already entered into your forms”. 🙌</p>
<p>I automated creating form instances via some lightweight meta-programming, which is a <a href='https://github.com//rubymonolith/demo/blob/cfc1ef83ee19816ffab1d62c11e6b95c0d589fc1/app/controllers/concerns/resource.rb#L17' title=''>work in progress</a>, but it’s easy to see how it’s possible to get back to building Rails applications without worrying about whether or not you forgot to permit a param.</p>
<p>This is just one of many examples Phlex will enable for better Rails forms. Oh, and it will be possible to embed these into Erb files too in case you want to keep using Erb.</p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-iuzqfjxt"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-iuzqfjxt"><span class="nt"><h1></span>Edit Post<span class="nt"></h1></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="no">Views</span><span class="o">::</span><span class="no">Posts</span><span class="o">::</span><span class="no">Form</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="vi">@post</span><span class="p">)</span> <span class="cp">%%></span>
</code></pre>
</div>
</div><h2 id='table-component' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#table-component' aria-label='Anchor'></a>Table component</h2>
<p>HTML tables are always an interesting exercise in building components. Here’s what a basic table component looks like at the time of this writing.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-lbv8mvzh"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-lbv8mvzh"><span class="k">class</span> <span class="nc">PostsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">resources</span> <span class="ss">:posts</span><span class="p">,</span> <span class="ss">from: :current_user</span>
<span class="k">class</span> <span class="nc">Index</span> <span class="o"><</span> <span class="no">ApplicationView</span>
<span class="nb">attr_writer</span> <span class="ss">:posts</span><span class="p">,</span> <span class="ss">:current_user</span>
<span class="k">def</span> <span class="nf">title</span> <span class="o">=</span> <span class="s2">"</span><span class="si">#{</span><span class="vi">@current_user</span><span class="p">.</span><span class="nf">name</span><span class="si">}</span><span class="s2">'s Posts"</span>
<span class="k">def</span> <span class="nf">template</span>
<span class="n">render</span> <span class="no">TableComponent</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">items: </span><span class="vi">@posts</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">table</span><span class="o">|</span>
<span class="n">table</span><span class="p">.</span><span class="nf">column</span><span class="p">(</span><span class="s2">"Title"</span><span class="p">)</span> <span class="p">{</span> <span class="n">show</span><span class="p">(</span><span class="n">_1</span><span class="p">,</span> <span class="ss">:title</span><span class="p">)</span> <span class="p">}</span>
<span class="n">table</span><span class="p">.</span><span class="nf">column</span> <span class="k">do</span> <span class="o">|</span><span class="n">column</span><span class="o">|</span>
<span class="c1"># Titles might not always be text, so we need to handle rendering</span>
<span class="c1"># Phlex markup within.</span>
<span class="n">column</span><span class="p">.</span><span class="nf">title</span> <span class="k">do</span>
<span class="n">link_to</span><span class="p">(</span><span class="n">user_blogs_path</span><span class="p">(</span><span class="vi">@current_user</span><span class="p">))</span> <span class="p">{</span> <span class="s2">"Blogs"</span> <span class="p">}</span>
<span class="k">end</span>
<span class="n">column</span><span class="p">.</span><span class="nf">item</span> <span class="p">{</span> <span class="n">show</span><span class="p">(</span><span class="n">_1</span><span class="p">.</span><span class="nf">blog</span><span class="p">,</span> <span class="ss">:title</span><span class="p">)</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="k">end</span>
</code></pre>
</div>
</div>
<p>I want this component to accept a collection of objects, in this case an Active Record association, that I can loop through to emit table rows.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-mklmwyft"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-mklmwyft"><span class="no">TableComponent</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">items: </span><span class="vi">@posts</span><span class="p">)</span>
</code></pre>
</div>
</div>
<p>Then the component configures columns that can accept text for the column title and a link to the blog post. The <code>_1</code> is a short cut for getting the first item, in our case a <code>Post</code> instance, and the <code>show</code> method renders a link to the post that uses <code>Post.title</code> for the link text.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-vda7y4hk"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-vda7y4hk"><span class="n">table</span><span class="p">.</span><span class="nf">column</span><span class="p">(</span><span class="s2">"Title"</span><span class="p">)</span> <span class="p">{</span> <span class="n">show</span><span class="p">(</span><span class="n">_1</span><span class="p">,</span> <span class="ss">:title</span><span class="p">)</span> <span class="p">}</span>
</code></pre>
</div>
</div>
<p>Or for a more complex use case, we could include a link in the <code>column.title</code> block so the user can get back to their blogs and display a link to the <code>post.title</code> in each <code>column.item</code>.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-j4qo39g5"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-j4qo39g5"><span class="n">table</span><span class="p">.</span><span class="nf">column</span> <span class="k">do</span> <span class="o">|</span><span class="n">column</span><span class="o">|</span>
<span class="c1"># Column title is markup, so we have to pass Phlex into</span>
<span class="c1"># the column.title to generate the link.</span>
<span class="n">column</span><span class="p">.</span><span class="nf">title</span> <span class="k">do</span>
<span class="n">link_to</span><span class="p">(</span><span class="n">user_blogs_path</span><span class="p">(</span><span class="vi">@current_user</span><span class="p">))</span> <span class="p">{</span> <span class="s2">"Blogs"</span> <span class="p">}</span>
<span class="k">end</span>
<span class="c1"># Then we link to each posts blog and print the title.</span>
<span class="n">column</span><span class="p">.</span><span class="nf">item</span> <span class="p">{</span> <span class="n">show</span><span class="p">(</span><span class="n">_1</span><span class="p">.</span><span class="nf">blog</span><span class="p">,</span> <span class="ss">:title</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>What’s remarkable about Phlex is how easy it is to mix helpers and markup together, essentially building your own markup language unique to your app that emits HTML in the browser.</p>
<h2 id='why-build-an-application-from-components' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#why-build-an-application-from-components' aria-label='Anchor'></a>Why build an application from components?</h2>
<p>There’s so many reasons, and there’s a <a href='https://www.chromatic.com/blog/component-driven-development/' title=''>lot of</a> <a href='https://addyosmani.com/first/' title=''>lists that</a> <a href='https://itnext.io/a-guide-to-component-driven-development-cdd-1516f65d8b55' title=''>use buzzwords</a> like TDD, composability, collaboration. I don’t really like buzzwords, so lets try to break down the benefits in terms of how it will help <em>you</em>.</p>
<h3 id='components-are-easier-to-change' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#components-are-easier-to-change' aria-label='Anchor'></a>Components are easier to change</h3>
<p>As your application UI becomes more complex, it becomes harder to change, especially if there’s a lot of HTML that’s been copy and pasted all over templates, partials, and view helpers.</p>
<p>Rails partials can make this slightly more manageable for simple chunks of UI, but it starts to get complicated when you need partials to deal with stateful UI components like tabs, filtered lists, or layouts.</p>
<p>Components keep all of this complexity in one place, making it easier to reason through when it needs to be changed.</p>
<h3 id='direct-access-to-view-state-makes-it-more-palatable-to-build-sophisticated-uis' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#direct-access-to-view-state-makes-it-more-palatable-to-build-sophisticated-uis' aria-label='Anchor'></a>Direct access to view state makes it more palatable to build sophisticated UIs</h3>
<p>Complex views usually have state unique to the view that is helpful to manage. For forms we can track what fields the view is displaying so we can automatically permit its params. For tables we could manage the order its sorted. For a bulk selector we might need to show checkboxes next to selected items. Its awkward managing this state in an Erb file, but it feels natural when this data is part of the view component itself.</p>
<h3 id='components-can-be-directly-tested' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#components-can-be-directly-tested' aria-label='Anchor'></a>Components can be directly tested</h3>
<p>Have you ever tried to write tests that test one Rails partial? Probably not, because there’s no way to directly pass them different variables. The only way of testing partials is to call them from a Rails view test. You could make a pretty good argument that its acceptable to implicitly test a component through a view, but if you’re trying to build a component library that’s an unsatisfactory answer.</p>
<h3 id='components-can-be-shared-with-other-people-inside-your-team-or-to-a-wider-community' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#components-can-be-shared-with-other-people-inside-your-team-or-to-a-wider-community' aria-label='Anchor'></a>Components can be shared with other people inside your team or to a wider community</h3>
<p>Medium size teams usually have engineers and designers working together to ship product—its helpful when they can collaborate together on a component together that’s part of the design system, then implement it throughout the Rails application.</p>
<p>If you’ve ever wondered why it’s so awkward to work with Rails plugins that generate UI in your app, it’s because they usually generate Erb templates with markup that doesn’t match your style of markup. If we can get to a world of standard UI components, we might get to a place where Rails plugin UIs work better out of the box.</p>
<h2 id='why-not-build-an-application-from-components' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#why-not-build-an-application-from-components' aria-label='Anchor'></a>Why not build an application from components?</h2>
<p>First, this approach isn’t for everybody—some folks loath abstractions and want to write markup. If that’s you then keep writing Erb templates, partials, and layouts in Rails.</p>
<p>There’s also certain applications where component-base UI’s don’t make a ton of sense, like websites that are primarily managing unstructured content. Heck even I prefer using Markdown and Erb files for content-heavy sites with <a href='https://sitepress.cc' title=''>Sitepress</a>.</p>
<p>Building an HTML UI entirely from Ruby Phlex components seems like a terrible idea initially, much like how the concept of building an application from TailwindCSS components initially seems like a terrible idea. It’s worth pushing past this initial instinct before making final judgement because it could end up feeling great.</p>
<h2 id='more-components' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#more-components' aria-label='Anchor'></a>More components</h2>
<p>I could write several articles about all sorts of different components that Phlex promises to make a little easier to implement. Maybe I’ll write about them in the future, but for now I’ll keep it as a list.</p>
<ul>
<li><strong class='font-[550] text-navy-950'>Tables</strong> - Tables could include helpers that make columns sortable in ascending or descending order, filters, lazy loading, and even syncing to an Turbo stream.
</li><li><strong class='font-[550] text-navy-950'>RESTful link helpers</strong> - Rails <code>link_to</code> helpers can get awkward, so I created RESTful link helpers like <code>delete(@post, role: "button") { "Delete Post" }</code> to replace long-winded calls like <code>link_to "Delete Post", @post, role: "button", data: { "turbo-method": :delete }</code>.
</li><li><strong class='font-[550] text-navy-950'>Bulk object selection</strong> - Display checkboxes next to each item in a collection of Active Record objects that your user can select and do something with—Phlex components and thoughtful use of Action classes could make bulk selection a breeze.
</li><li><strong class='font-[550] text-navy-950'>Navigation</strong> - Display a sidebar or top menu navigation bar that shows a page is “active” if a person is on that part of the website. Describe the structure of your menu once and designate what you want to display on a mobile drop-down vs a desktop nav bar.
</li><li><strong class='font-[550] text-navy-950'>Async query rendering</strong> - **** Components can be built that manage the loading behavior of a page, like adding a turbo frame to a Phlex component that is populated when an <a href='https://pawelurbanek.com/rails-load-async' title=''>Active Record <code>#loan_async</code></a> query completes.
</li><li><strong class='font-[550] text-navy-950'>Hotwire integration</strong> - Build out a view framework that automatically keeps in sync with the server, similar to how Elixir LiveView works.
</li></ul>
<p>The best part? These could be built, packaged up into gems, and shared to the wider community. When a community of developers build up a standard library of view components, more powerful abstractions can be built on top of them making everybody even <em>more</em> productive.</p>
/ruby-dispatch/component-driven-development-on-rails-with-phlex/assets/thumbnail.webpHumane Rails Formshttps://fly.io/ruby-dispatch/humane-rails-forms/2023-04-12T00:00:00+00:002023-11-21T21:08:38+00:00<p><strong class='font-[550] text-navy-950'>How many times have you cursed at a date form field because it rejected <code>03/01/22</code> and wanted <code>03/01/2023</code>? Number inputs are the same—what if instead of copy & pasting from the calculator app you could enter <code>79 / 2</code> in a field and have it evaluate to <code>39.5</code>? Inputomatic makes this possible in your Rails app, which means less friction in your UIs for the people who use your app.</strong></p>
<p>It’s 2023 and here’s the best answer web browsers have given us to enter dates & times on popular browsers like Safari.</p>
<p><img alt="Date picker in Safari" src="/images/o907dZUtwtWdmffkJTwZOLYS.png" /></p>
<p>And Chrome.</p>
<p><img alt="Date picker in Chrome" src="/images/rsa__ZTo6fcGg7Y8aD-GdXqy.png" /></p>
<p>What’s maddening about them is they force people to conform to the computer. It should be the other way around—the computer should be more forgiving to the messiness of the human condition and conform to our imprecise inputs.</p>
<p>Let’s see if we can do better in one of the worlds most exciting and reviled class of applications: the expense reports!</p>
<h2 id='an-expense-reporting-app' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#an-expense-reporting-app' aria-label='Anchor'></a>An Expense Reporting App</h2>
<p>By far one of the most frustrating experiences of our lives is entering expense report data. Setting the date involves clicking on a bunch of different arrows, buttons, etc. to set the date on some poorly designed picker. Then, there’s always some policy that says “expense half of your cellphone bill”, we have to open a calculator to do simple calculations like <code>124.99 / 2</code>.</p>
<p>What if we could build forms that made sense of inputs like <code>Last week</code> for dates and <code>124.99 / 2</code> for numbers?</p>
<p><img alt="Form that accepts inputs like "Last week" for a date and "124.99/2" for a number" src="/images/rGXCbKYJoD0uLhyRbnuArb2i.png" /></p>
<p>That’s a little better! When we save it we get what we’d expect: a date from last week and the calculated cost.</p>
<p><img alt="Computed values are persisted to the database when forms are saved" src="/images/YJQxdIDdvKdEe3ronCBxTpW6.png" /></p>
<p>Let’s try creating an expense from 900 days ago, which is outside of the “90 days ago” policy in our expense account:</p>
<p><img alt="Original values are preserved if there's a model validation error" src="/images/APio78Gz8Qh7WE_tWc5-BvFF.png" /></p>
<p>Rails preserves the input value until the validation is cleared, then it persists the actual date or number to the database.</p>
<p><a href='https://inputomatic-demo.fly.dev/expenses/new' title=''>Try the demo app</a> and enter a new expense. I promise it will be one of the most luxurious expenses you’ve ever entered. You can also <a href='https://github.com/rocketshipio/inputomatic-demo' title=''>clone the demo app</a> and run it in your own machine or <a href='https://fly.io/docs/rails/' title=''>deploy it to Fly.io</a>.</p>
<p>How does that work exactly?</p>
<h2 id='the-activemodel-attribute-class-method' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-activemodel-attribute-class-method' aria-label='Anchor'></a>The ActiveModel <code>attribute</code> class method</h2>
<p>When we look at the <code>expense.rb</code> <a href='https://github.com/rocketshipio/inputomatic-demo/blob/main/app/models/expense.rb' title=''>Active Record model code</a>, we see the class method <code>attribute</code> and pass it an <code>Inputomatic</code> class the casts the values to and from the model, form, and the database.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-vo8vewe0"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-vo8vewe0"><span class="k">class</span> <span class="nc">Expense</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">attribute</span> <span class="ss">:purchased_at</span><span class="p">,</span> <span class="no">Inputomatic</span><span class="o">::</span><span class="no">DateTime</span><span class="p">.</span><span class="nf">new</span>
<span class="n">attribute</span> <span class="ss">:amount</span><span class="p">,</span> <span class="no">Inputomatic</span><span class="o">::</span><span class="no">Number</span><span class="p">.</span><span class="nf">new</span>
<span class="n">validates</span> <span class="ss">:purchased_at</span><span class="p">,</span>
<span class="ss">inclusion: </span><span class="p">{</span>
<span class="ss">in: </span><span class="no">Range</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">90</span><span class="p">.</span><span class="nf">days</span><span class="p">.</span><span class="nf">ago</span><span class="p">,</span> <span class="mi">90</span><span class="p">.</span><span class="nf">days</span><span class="p">.</span><span class="nf">from_now</span><span class="p">),</span>
<span class="ss">message: </span><span class="o">-></span> <span class="p">(</span><span class="n">object</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span> <span class="p">{</span> <span class="s2">"</span><span class="si">#{</span><span class="n">data</span><span class="p">[</span><span class="ss">:value</span><span class="p">].</span><span class="nf">to_date</span><span class="si">}</span><span class="s2"> is not between </span><span class="si">#{</span><span class="mi">90</span><span class="p">.</span><span class="nf">days</span><span class="p">.</span><span class="nf">ago</span><span class="p">.</span><span class="nf">to_date</span><span class="si">}</span><span class="s2"> and </span><span class="si">#{</span><span class="mi">90</span><span class="p">.</span><span class="nf">days</span><span class="p">.</span><span class="nf">from_now</span><span class="p">.</span><span class="nf">to_date</span><span class="si">}</span><span class="s2">"</span> <span class="p">}</span>
<span class="p">}</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p><a href='https://github.com/rocketshipio/inputomatic' title=''>Inputomatic</a> is a gem I created that defines a few casting behaviors between the value in the Active Record model and the database. Turns out you can do some pretty nifty things with <code>ActiveRecord::Type</code> classes that make forms more of a joy to use.</p>
<h3 id='the-activerecord-type-value-cast_value-method' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-activerecord-type-value-cast_value-method' aria-label='Anchor'></a>The <code>ActiveRecord::Type::Value#cast_value</code> method</h3>
<p>Let’s have a look at the <code>Inputomatic::Number</code> class, which is a subclass of <code>activerecord::Type::Value</code>:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-o4ifhe9j"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-o4ifhe9j"><span class="k">class</span> <span class="nc">Number</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Type</span><span class="o">::</span><span class="no">Value</span>
<span class="k">def</span> <span class="nf">cast_value</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="n">value</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">String</span><span class="p">)</span> <span class="p">?</span> <span class="no">ArithmeticInterpreter</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">value</span><span class="p">).</span><span class="nf">parse</span> <span class="p">:</span> <span class="n">value</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>The <code>cast_value</code> method is where all the magic happens. If the value is a <code>String</code>, it’s coming from a form as an arithmetic expression. <a href='https://github.com/rocketshipio/inputomatic/blob/main/lib/inputomatic/arithmetic_interpreter.rb' title=''>Inputomatic’s arithmetic interpreter</a> (which was <a href='https://fly.io/ruby-dispatch/pairing-with-gpt-4/' title=''>written by my pair bear, GPT-4</a>) parses the string <code>124.99/2</code> into an expression that it can safely evaluate (never <em>ever</em> run form input through <code>Kernel#eval</code>, which Stack Overflow would tell you to do) into a number.</p>
<p>Once we have a number, case closed! We have a float, integer, or decimal that can be persisted to the database.</p>
<p>If the value is <em>not</em> a <code>String</code>, then we just return the value assuming it will be handled by any upstream handlers. In this case we’ll assume its an <code>Integer</code> or <code>Float</code>.</p>
<figure class="post-cta">
<figcaption>
<h1>Fly.io ❤️ Rails</h1>
<p>Fly.io is a great way to run your Rails app close to your users. It’s really easy to get started. You can be running in minutes.</p>
<a class="button bg-white text-violet-500 hover:bg-opacity-75 hover:text-violet-700 transition-colors" href="https://fly.io/docs/rails/">
Deploy a Rails app today! <span class='opacity:50'>→</span>
</a>
</figcaption>
<div class="image-container">
<img src="/static/images/cta-rabbit.webp" srcset="/static/images/cta-rabbit@2x.webp 2x" alt="">
</div>
</figure>
<h2 id='quot-pickers-quot-and-humanized-form-inputs-can-co-exist' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#quot-pickers-quot-and-humanized-form-inputs-can-co-exist' aria-label='Anchor'></a>“Pickers” and humanized form inputs can co-exist</h2>
<p>If you still love date pickers, color pickers, etc. you can keep them! Humanized form inputs would read these values and do less interpretation to them before casting them into values the database can understand.</p>
<p>For people who want to skip the pickers, they’d get a better experience. For people who love clicking around on pickers, that would all still work too.</p>
<h2 id='limitations' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#limitations' aria-label='Anchor'></a>Limitations</h2>
<p>There’s always room for mistakes when a machine interprets something a human enters. It’s important to understand how important it is to capture precise information. For example, when we enter <code>last week</code> does that mean <code>7 days ago</code> or <code>The last day of last week</code>?</p>
<p>For an expense report application, there’s probably a policy that checks the dates to make sure it’s within the past 90 days. If the arithmetic of a float is off by $0.000000000001, its not the end of the world.</p>
<p>There are localization issues to think through too—for example in some countries one thousand is written as <code>1,000</code> and in others it’s written as <code>1.000</code>.</p>
<h3 id='rails-validations' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#rails-validations' aria-label='Anchor'></a>Rails validations</h3>
<p>Additionally, some Rails validations, like <code>validates :amount, numericality: { greater_than: 0 }</code> don’t work out of the box because they do regular expression tests against strings to check if they’re floats or integers.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-wdnbpkz5"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-wdnbpkz5"><span class="c1"># Rails validations check the string with regular expressions before the value is cast by `Inputomatic::Number.new`, which</span>
<span class="c1"># makes these validations not work properly.</span>
<span class="n">validates</span> <span class="ss">:amount</span><span class="p">,</span>
<span class="ss">numericality: </span><span class="p">{</span> <span class="ss">greater_than_or_equal_to: </span><span class="mi">0</span> <span class="p">}</span>
</code></pre>
</div>
</div>
<p>If you tried to add a numericality validation to the demo, it would complain that <code>124.99/2</code> is not a number. This validation could probably be patched to perform validations after the value is cast.</p>
<h3 id='chronice-time-parsing-gem' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#chronice-time-parsing-gem' aria-label='Anchor'></a>Chronice Time parsing gem</h3>
<p>Inputomatic uses the <a href='https://github.com/mojombo/chronic' title=''>Chronic gem</a> to parse dates, which has a lot of <a href='https://github.com/mojombo/chronic/issues' title=''>issues</a> and <a href='https://github.com/mojombo/chronic/pulls' title=''>PRs</a> that are worth understanding to get a feel for how well date parsing works.</p>
<p>If you enter the value <code>March 30</code> into a form on April 1, 2023, you’d be surprised to get back <code>March 30, 2024</code>. That’s because Chronic is currently only setup to handle partial dates in the future or the past—there’s no way to tell Chronic to “assume the current year if none is given”.</p>
<p>You could set Chronic’s context for the past, but then you’d be surprised to get the date <code>December 1, 2022</code> when you enter the value <code>December 1</code>.</p>
<h2 id='more-ways-to-make-your-application-forms-more-humane' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#more-ways-to-make-your-application-forms-more-humane' aria-label='Anchor'></a>More ways to make your application forms more humane</h2>
<p>This technique could be used in a lot of places including:</p>
<ul>
<li><strong class='font-[550] text-navy-950'>Telephone numbers</strong> - Cast numbers like <code>800flowers</code> into numbers or infer the country code with a gem like <a href='https://github.com/floere/phony' title=''>Phoney</a>.
</li><li><strong class='font-[550] text-navy-950'>Countries or states</strong> - If somebody enters <code>US</code>, <code>USA</code>, <code>United States of America</code>, etc. into a form field for country, the country code could be properly resolved when the value is cast. Same for states! The <a href='https://github.com/countries/countries' title=''>Countries gem</a> could be a good start.
</li><li><strong class='font-[550] text-navy-950'>Colors</strong> - Convert “black” to <code>#000</code>, “white” to <code>#fff</code>, etc.
</li><li><strong class='font-[550] text-navy-950'>URLs</strong> - If you want to <em>always</em> ensure URLs have <code>https</code>, you could cast the String value into a <code>URL</code> object, set <code>URI#scheme = "https"</code>, then save out the string into the database so it’s always https. Maybe you just want to always strip off the trailing slash.
</li><li><strong class='font-[550] text-navy-950'>Capitalization</strong> - If you have fields that must be stored in lowercase on the database, you could do it for the user automatically with a LowerCase type that always lower cases values.
</li><li><strong class='font-[550] text-navy-950'>Location</strong> - I don’t recommend running strings through expensive API calls when casting a value, but if you can make the call “fast and cheap” enough, there’s nothing stopping you from converting a location like “Mexico City, Mexico” into a latitude longitude coordinate.
</li></ul>
<p>Whatever you choose, just make sure the wiggle room is acceptable for the application and the casting is fast enough.</p>
/ruby-dispatch/humane-rails-forms/assets/cover-thumb.webpIntroducing Live Elementshttps://fly.io/ruby-dispatch/introducing-live-elements/2023-03-28T00:00:00+00:002023-11-21T21:08:38+00:00<p>Ruby on Rails <a href='https://rubyonrails.org/' title=''>contains everything you need</a>:</p>
<blockquote>
<p>Rendering HTML templates, updating databases, sending and receiving emails,
maintaining live pages via WebSockets, enqueuing jobs for asynchronous work,
storing uploads in the cloud, providing solid security protections for common
attacks. Rails does it all and so much more.</p>
</blockquote>
<p>But I still want more. <a href='https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html' title=''>Phoenix LiveView</a> supports <a href='https://dockyard.com/blog/2018/12/12/phoenix-liveview-interactive-real-time-apps-no-need-to-write-javascript' title=''>Interactive, Real-Time Apps.
No Need to Write JavaScript</a>.
Yea, I definitely want that. I want to build applications like the following with <em>no</em> custom JavaScript:</p>
<div class="flex flex-col lg:grid grid-cols-3 gap-4 mx-auto my-12"><p><a class="block mx-auto" href="/ruby-dispatch/2023-03-28/validate.gif">
<video title="Real time form validation" autoplay="autoplay" loop="loop" muted="muted" playsinline="playsinline" disablepictureinpicture="true" class="mb-8" src="/videos/validate.mp4?card&border"></video>
</a>
<a class="block mx-auto" href="/ruby-dispatch/2023-03-28/search.gif">
<video title="Live search" autoplay="autoplay" loop="loop" muted="muted" playsinline="playsinline" disablepictureinpicture="true" class="mb-8" src="/videos/search.mp4?card&border"></video>
</a>
<a class="block mx-auto" href="/ruby-dispatch/2023-03-28/markdown.gif">
<video title="Markdown previews" autoplay="autoplay" loop="loop" muted="muted" playsinline="playsinline" disablepictureinpicture="true" class="mb-8" src="/videos/markdown.mp4?card&border"></video>
</a></p>
</div>
<p>While what I am about to present doesn’t satisfy all the use cases that Phoenix
LiveView supports, it does support a huge chunk of them. And does so in a way
that both builds on what Rails already offers and does so in a way that will be
instantly familiar to Rails developers.</p>
<p>Once this stabilizes we can move on to the next chunk.</p>
<h2 id='what-we-have-to-work-with' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#what-we-have-to-work-with' aria-label='Anchor'></a>What we have to work with</h2>
<p>There is really no need to start from scratch. <a href='https://hotwired.dev/' title=''>Hotwire</a> already has some awesome building blocks for us to use. First <a href='https://turbo.hotwired.dev/' title=''>Turbo Streams</a>:</p>
<blockquote>
<p><em>Turbo Streams</em> deliver page changes over WebSocket, SSE or in response to
form submissions using just HTML and a set of CRUD-like actions.</p>
</blockquote>
<p>And <a href='https://stimulus.hotwired.dev/handbook/introduction' title=''>Stimulus</a>:</p>
<blockquote>
<p><em>actions</em>, which connect controller methods to DOM events using data-action attributes</p>
</blockquote>
<p>The difference here is that what we want is to associate DOM events to
<strong class='font-[550] text-navy-950'>server</strong> controller events. That what Live Elements does. Without WebSockets
or long polling. Or you needing to write even a single line of JavaScript
code.</p>
<h2 id='demo-time' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#demo-time' aria-label='Anchor'></a>Demo time!</h2>
<p>While it is said that a picture is worth a thousand words, a demo can be priceless.</p>
<p>For the impatient, a
<a href='./live-elements-demo.txt' title=''><code>live_elements_demo</code></a>
shell script is provided which you can copy and paste into a terminal window
running a bash or dash shell and get up and running in seconds.</p>
<p>Let’s start with a modest Rails form, decorated with <code>data</code> attributes:</p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-3xahe85s"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-3xahe85s"><span class="nt"><div></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="ss">partial: </span><span class="s1">'header'</span><span class="p">,</span> <span class="ss">locals: </span><span class="p">{</span><span class="ss">color: </span><span class="s2">"yellow"</span><span class="p">}</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form_with</span> <span class="ss">data: </span><span class="p">{</span><span class="ss">controller: </span><span class="s2">"live-elements"</span><span class="p">}</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">button</span> <span class="s2">"blue"</span><span class="p">,</span> <span class="ss">name: </span><span class="s1">'color'</span><span class="p">,</span>
<span class="ss">data: </span><span class="p">{</span><span class="ss">action: </span><span class="p">{</span><span class="ss">click: </span><span class="n">demo_click_path</span><span class="p">}},</span>
<span class="ss">class: </span><span class="s2">"bg-blue-500 hover:bg-blue-700 text-white
font-bold py-2 px-4 rounded"</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">button</span> <span class="s2">"red"</span><span class="p">,</span> <span class="ss">name: </span><span class="s1">'color'</span><span class="p">,</span>
<span class="ss">data: </span><span class="p">{</span><span class="ss">action: </span><span class="p">{</span><span class="ss">click: </span><span class="n">demo_click_path</span><span class="p">}},</span>
<span class="ss">class: </span><span class="s2">"bg-red-500 hover:bg-red-700 text-white
font-bold py-2 px-4 rounded"</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>
</div>
<p>Everything here is standard Ruby on Rails. In fact, it looks like we are using
a Stimulus controller, and that’s because we are. Live Elements is a Stimulus
controller, just one that looks for actions and routes them to the server via
<a href='https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch' title=''>fetch</a>
requests rather than to client side JavaScript.</p>
<p>In addition there are two data actions defined. Both associate DOM click
events to server side routes. You are free to use any DOM event and any server
route. In most cases HTTP POST will be used, but if the element is associated
with a form, the method from the form will be used.</p>
<p>Now let’s look at the partial:</p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-l5cxclkp"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-l5cxclkp"> <span class="cp"><%=</span> <span class="n">turbo_frame_tag</span> <span class="s2">"header"</span><span class="p">,</span>
<span class="ss">class: </span><span class="s2">"block bg-</span><span class="si">#{</span><span class="n">color</span><span class="si">}</span><span class="s2">-400 mb-4"</span> <span class="k">do</span> <span class="cp">%></span>
<span class="nt"><h1</span> <span class="na">class=</span><span class="s">"font-bold text-4xl"</span><span class="nt">></span>Live button demo<span class="nt"></h1></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="c"><!-- bg-yellow-400 bg-blue-400 bg-red-400 --></span>
</code></pre>
</div>
</div>
<p>Not much to see here. The color is the only part that changes. Note the
comment at the bottom will get Tailwind to include these classes in the CSS
that is sent to the client..</p>
<p>Finally, the controller:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-5tyicw84"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-5tyicw84"><span class="k">class</span> <span class="nc">DemoController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="k">def</span> <span class="nf">button</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">click</span>
<span class="n">respond_to</span> <span class="k">do</span> <span class="o">|</span><span class="nb">format</span><span class="o">|</span>
<span class="nb">format</span><span class="p">.</span><span class="nf">turbo_stream</span> <span class="p">{</span>
<span class="n">render</span> <span class="ss">turbo_stream: </span><span class="n">turbo_stream</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="s1">'header'</span><span class="p">,</span>
<span class="n">render_to_string</span><span class="p">(</span><span class="ss">partial: </span><span class="s1">'header'</span><span class="p">,</span>
<span class="ss">locals: </span><span class="p">{</span><span class="ss">color: </span><span class="n">params</span><span class="p">[</span><span class="ss">:color</span><span class="p">]}))</span>
<span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Again, a standard Rails controller, with two actions. One accesses a <code>param</code>
from the request and responds with a turbo stream response. In this case, the
part of DOM identified with an id of <code>header</code> will be replaced with the
rendering of the partial. Any <a href='https://turbo.hotwired.dev/reference/streams' title=''>Turbo Stream
action</a> can be used, and you can
even respond with multiple actions by rendering a <a href='https://turbo.hotwired.dev/handbook/streams#stream-messages-and-actions' title=''>view containing
<code>turbo-stream</code>
templates</a>.</p>
<p>So far, not a single line of JavaScript was written. I promised that there would
be no custom JavaScript. So without further ado, lets install Live Element:</p>
<div class="highlight-wrapper group relative sh">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-35dmsqvr"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-35dmsqvr">bin/importmap pin @flydotio/stimulus-live-elements@0.1.0
<span class="nb">echo</span> <span class="s1">'export { default } from "@flydotio/stimulus-live-elements"'</span> <span class="o">></span> <span class="se">\</span>
app/javascript/controllers/live_elements_controller.js
</code></pre>
</div>
</div>
<p>Hopefully in the future there will be a standard way to install third party
Stimulus controllers, but for now this will do.</p>
<p>All that is left is to wire up some routes:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-imrelg96"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-imrelg96"><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
<span class="n">root</span> <span class="s2">"demo#button"</span>
<span class="n">post</span> <span class="s2">"demo/click"</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Running <code>bin/dev</code> and navigating to the site will produce
<a href='buttons.gif' title=''>this masterpiece</a>.</p>
<p>This demo isn’t very fancy, and to be honest could have been done without
TurboStream at all and would have had roughly the same user experience even
though it would be sending complete rendering of the page over the wire.</p>
<p><a href='https://github.com/fly-apps/live-elements-demo/blob/main/README.md#readme' title=''>Check out the three demos you saw above</a> - run them locally or deploy to fly.io.
Each of these demos were created without any custom JavaScript, just
vanilla views, partials, and controllers returning Turbo Stream responses.</p>
<figure class="post-cta">
<figcaption>
<h1>You can play with this right now.</h1>
<p>Deploy using [Fly.io terminal](https://fly.io/terminal) or see our [Hands-on](https://fly.io/docs/hands-on/) guide that will walk you through the steps.</p>
<a class="button bg-white text-violet-500 hover:bg-opacity-75 hover:text-violet-700 transition-colors" href="https://fly.io/docs/rails/">
Try Fly for free <span class='opacity-50'>→</span>
</a>
</figcaption>
<div class="image-container">
<img src="/static/images/cta-rabbit.webp" srcset="/static/images/cta-rabbit@2x.webp 2x" alt="">
</div>
</figure>
<p>Since this is all vanilla HTTP request/response, things like supporting
sessions is possible and will scale with existing web servers with
processes/threads. What it doesn’t have is any notion of server push or server
side actors, so subscribing to server events would still have to be done via
ActionCable. </p>
<p>Hopefully this inspires ideas and use cases. Drag and drop, keyboard events,
and more should be possible but would require a change to the library. Try it
out. Start a
<a href='https://github.com/superfly/stimulus-live-elements/discussions' title=''>discussion</a>,
create an <a href='https://github.com/superfly/stimulus-live-elements/issues' title=''>issue</a> or
open a <a href='https://github.com/superfly/stimulus-live-elements/pulls' title=''>pull request</a>.</p>
<hr>
<h2 id='technical-details' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#technical-details' aria-label='Anchor'></a>Technical details</h2>
<p>For those that want to know more about how this works under the covers:</p>
<ul>
<li><a href='https://fly.io/ruby-dispatch/turbostream-fetch/' title=''>Using TurboStream With the Fetch API</a> covers how to produce a correct <code>X-CSRF-Token</code> and how to use <code>Turbo.renderStreamMessage</code>.
</li><li>Form parameters are passed using search parameters when sent via HTTP get, and
are passed using <code>x-www-form-urlencoded</code> body for all other HTTP methods.
</li><li><a href='https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver' title=''>MutationObserver</a> is used to detect changes to the DOM, and additions are scanned for <code>data-action</code> attributes and event listeners are attached to such elements.
</li><li>Actions are processed sequentially in the order that they are received, and
<a href='https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback' title=''>requestIdleCallback</a> is used to ensure that turbo stream actions are applied before the next action is taken. As Safari <a href='https://caniuse.com/requestidlecallback' title=''>does not yet support this API</a>, a <code>setTimeout</code> with a value of 50 is used instead.
</li><li>While the <a href='https://turbo.hotwired.dev/handbook/streams#streaming-from-http-responses' title=''>documentation</a> doesn’t mention it, and <a href='https://github.com/hotwired/turbo-rails/issues/77#issuecomment-757349251' title=''>DHH would disagree</a>,
<code>render turbo_stream</code> can be passed an array of turbo stream actions to be applied. The search demo above flashed the screen when the entire output is replaced, but is smooth when individual rows added/removed using individual <code>remove</code>, <code>after</code>, and <code>prepend</code> actions.
</li></ul>
<hr>
<h2 id='appendix-comparison-with-other-frameworks' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#appendix-comparison-with-other-frameworks' aria-label='Anchor'></a>Appendix: Comparison with other Frameworks</h2>
<p>While Phoenix LiveView is the gold standard here, I’m going to limit this
discussion to components that can be used with Rails applications. The
general theme is that the others are more mature but don’t build on
TurboStreams so are modestly larger in size, may involve “some assembly
required” to get started, and introduce a different programming model
than one finds in Rails applications.</p>
<p>And the good news is that you can use Live Elements in conjunction with
other frameworks. So not only can you get started quicker with Live Elements
you can switch when needed to a more capable framework should you
end up needing it. For many cases,
<a href='https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it' title=''>YAGNI</a> applies.</p>
<h3 id='htmx' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#htmx' aria-label='Anchor'></a>HTMX</h3>
<p>This is the closest conceptual cousin to Live Elements. Instead of <code>data-</code>
attributes, you will use <code>hx-</code> attributes, and more of them. That’s because
with HTMX you specify things like the target and swap strategies using
attributes. Rails helpers encourage the use of <code>data-</code> attributes so
there may be some additional minor syntax advantage here.</p>
<p>More importantly, with Live Elements all of this is controlled by the response
generated by the server, including an obvious way to respond back with
multiple DOM updates.</p>
<p>The end result is a consistent programming model between Turbo Streams
typically used with WebSockets and with Live Elements. In many cases
you will be able to share partials between the two.</p>
<h3 id='stimulusreflex' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#stimulusreflex' aria-label='Anchor'></a>StimulusReflex</h3>
<p>This is a much more ambitious and capable library. It requires web sockets.
It also is currently built on Webpacker.</p>
<p>With StimulusReflex you create “Reflex actions” instead of standard
controller actions, and even (*** <em>gasp</em> ***) write JavaScript controllers.
While there appears to be no reason why you can’t mix and match this
with Live Elements, if you find yourself needing what StimulusReflex
provides it is best to think of Live Elements as training wheels and
build a plan to converge over time to just StimulusReflex.</p>
<h3 id='cableready' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#cableready' aria-label='Anchor'></a>CableReady</h3>
<p>This is a prerequisite for StimulusReflex but can be used standalone. Again,
it requires web sockets, and currently presumes that you have bought into
the whole javascript ecosystem vs import maps.</p>
<p>While StimulusReflex may be seen as a potentially better alternatives to
Live Element, CableReady can be seen as an addition to ActionCable that
may compliment Live Element.</p>
<p><del>While CableReady uses a <a href='https://cableready.stimulusreflex.com/appendencies/implementation.html#json-wire-format' title=''>JSON wire format</a> as opposed to
TurboStream’s HTML fragment approach, it would be an interesting project
to map CableReady’s <a href='https://cableready.stimulusreflex.com/reference/operations/' title=''>operations</a> to a HTML syntax and to add
<a href='https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements' title=''>HTML custom elements</a> to process these operations too.
</del> See update:</p>
<h3 id='update-turbo-boost' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#update-turbo-boost' aria-label='Anchor'></a>Update: Turbo Boost</h3>
<p><a href='https://ruby.social/@marcoroth' title=''>Marco Roth</a>
<a href='https://ruby.social/@marcoroth/110102134330079859' title=''>pointed me to</a>
<a href='https://github.com/hopsoft/turbo_boost-streams' title=''>Turbo Boost Streams</a> which already does what the deleted paragraph above describes.</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-xj4bpqbx"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-xj4bpqbx">bundle add turbo_boost-streams --version 0.0.8
bin/importmap add @turbo-boost/streams@0.0.8
</code></pre>
</div>
</div>
<p>Add to the top of <code>app/javascript/controllers/application.js</code>:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-yjg3aqxz"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-yjg3aqxz">import "@turbo-boost/streams"
</code></pre>
</div>
</div>
<p>And then you can add turbo boost actions to your responses:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-ojb7u8rd"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-ojb7u8rd">stream << turbo_stream.invoke("console.log", args: ["Hello World!"])
</code></pre>
</div>
</div>
<p>Sweet!</p>
<h3 id='phlex' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#phlex' aria-label='Anchor'></a>Phlex</h3>
<p>This one probably doesn’t belong on the list as they serve different needs,
but I’m including it as it is worth pointing out that from a Phlex perspective
Live Elements are just <a href='https://www.phlex.fun/html/tags/' title=''>Hash Attributes</a>
so the approaches will work well together.</p>
<p>Phlex is also <a href='https://ruby.social/@joeldrapper/110077981036771392' title=''>looking into streaming
HTML</a> using <code>turbo-stream</code>
actions as they were intended to be used. Again, a good match.</p>
/ruby-dispatch/introducing-live-elements/assets/introducing-live-elements-thumbnail.webpPairing with GPT-4https://fly.io/ruby-dispatch/pairing-with-gpt-4/2023-03-23T00:00:00+00:002023-11-21T21:08:38+00:00<p><strong class='font-[550] text-navy-950'>GPT-4 can be helpful for beginner and senior Ruby developers, but it does have limitations. It won’t write all of your software for you, but it will point you in a useful direction, especially if you prefer learning by doing.</strong></p>
<p>Let’s look at how well GPT-4 pairing works by picking an easy, but less well known project with some edge cases: downloading a RubyGem, parsing the docs via YARN, and dumping them into a Sqlite database.</p>
<h2 id='the-initial-prompt' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-initial-prompt' aria-label='Anchor'></a>The initial prompt</h2>
<blockquote>
<p>Create a Ruby script for me that downloads a RubyGem, parses the docs via YARD, and dumps them into a Sqlite database that’s named <code>#{gem-name}-#{gem-version}.db</code>. The database should also include information from the Gem spec.</p>
</blockquote>
<p>GPT-4 usually starts its responses with an overview of what it’s about to do, then shows how it solves the problem, followed by recapping and summing up the solution.</p>
<h3 id='gpt-4-response' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#gpt-4-response' aria-label='Anchor'></a>GPT-4 response</h3>
<p>To create a Ruby script that downloads a RubyGem, parses the documentation using YARD, and stores the data in a SQLite database named with the gem name and version, you will need to follow these steps:</p>
<ol>
<li>Install the required gems:
<ul>
<li><code>gem install yard</code>
</li><li><code>gem install sqlite3</code>
</li><li><code>gem install bundler</code>
</li></ul>
</li><li>Create a new file named <code>gem_doc_parser.rb</code> and paste the following code:
</li></ol>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-lk1x2p9w"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-lk1x2p9w"><span class="nb">require</span> <span class="s1">'yard'</span>
<span class="nb">require</span> <span class="s1">'sqlite3'</span>
<span class="nb">require</span> <span class="s1">'bundler'</span>
<span class="k">def</span> <span class="nf">download_and_parse_gem</span><span class="p">(</span><span class="n">gem_name</span><span class="p">,</span> <span class="n">gem_version</span><span class="p">)</span>
<span class="n">gem_path</span> <span class="o">=</span> <span class="s2">"</span><span class="si">#{</span><span class="n">gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="n">gem_version</span><span class="si">}</span><span class="s2">"</span>
<span class="c1"># Download the gem</span>
<span class="no">Bundler</span><span class="o">::</span><span class="no">CLI</span><span class="p">.</span><span class="nf">start</span><span class="p">([</span><span class="s2">"gem"</span><span class="p">,</span> <span class="n">gem_name</span><span class="p">,</span> <span class="s2">"--version"</span><span class="p">,</span> <span class="n">gem_version</span><span class="p">])</span>
<span class="c1"># Parse the gem documentation using YARD</span>
<span class="no">YARD</span><span class="o">::</span><span class="no">Registry</span><span class="p">.</span><span class="nf">clear</span>
<span class="no">YARD</span><span class="o">::</span><span class="no">Parser</span><span class="o">::</span><span class="no">SourceParser</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">gem_path</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">create_database</span><span class="p">(</span><span class="n">gem_name</span><span class="p">,</span> <span class="n">gem_version</span><span class="p">)</span>
<span class="n">db</span> <span class="o">=</span> <span class="no">SQLite3</span><span class="o">::</span><span class="no">Database</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="n">gem_version</span><span class="si">}</span><span class="s2">.db"</span><span class="p">)</span>
<span class="c1"># Create tables for storing gem spec and documentation</span>
<span class="n">db</span><span class="p">.</span><span class="nf">execute</span> <span class="o"><<-</span><span class="no">SQL</span><span class="sh">
CREATE TABLE IF NOT EXISTS gem_spec (
id INTEGER PRIMARY KEY,
name TEXT,
version TEXT
);
</span><span class="no"> SQL</span>
<span class="n">db</span><span class="p">.</span><span class="nf">execute</span> <span class="o"><<-</span><span class="no">SQL</span><span class="sh">
CREATE TABLE IF NOT EXISTS gem_docs (
id INTEGER PRIMARY KEY,
path TEXT,
type TEXT,
doc TEXT
);
</span><span class="no"> SQL</span>
<span class="n">db</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">store_gem_spec</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">gem_name</span><span class="p">,</span> <span class="n">gem_version</span><span class="p">)</span>
<span class="n">db</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="s2">"INSERT INTO gem_spec (name, version) VALUES (?, ?)"</span><span class="p">,</span> <span class="p">[</span><span class="n">gem_name</span><span class="p">,</span> <span class="n">gem_version</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">store_docs</span><span class="p">(</span><span class="n">db</span><span class="p">)</span>
<span class="no">YARD</span><span class="o">::</span><span class="no">Registry</span><span class="p">.</span><span class="nf">all</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">object</span><span class="o">|</span>
<span class="n">path</span> <span class="o">=</span> <span class="n">object</span><span class="p">.</span><span class="nf">path</span>
<span class="n">type</span> <span class="o">=</span> <span class="n">object</span><span class="p">.</span><span class="nf">type</span>
<span class="n">doc</span> <span class="o">=</span> <span class="n">object</span><span class="p">.</span><span class="nf">docstring</span><span class="p">.</span><span class="nf">to_raw</span>
<span class="n">db</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="s2">"INSERT INTO gem_docs (path, type, doc) VALUES (?, ?, ?)"</span><span class="p">,</span> <span class="p">[</span><span class="n">path</span><span class="p">,</span> <span class="n">type</span><span class="p">,</span> <span class="n">doc</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">main</span>
<span class="nb">puts</span> <span class="s2">"Enter the gem name:"</span>
<span class="n">gem_name</span> <span class="o">=</span> <span class="nb">gets</span><span class="p">.</span><span class="nf">chomp</span>
<span class="nb">puts</span> <span class="s2">"Enter the gem version:"</span>
<span class="n">gem_version</span> <span class="o">=</span> <span class="nb">gets</span><span class="p">.</span><span class="nf">chomp</span>
<span class="n">download_and_parse_gem</span><span class="p">(</span><span class="n">gem_name</span><span class="p">,</span> <span class="n">gem_version</span><span class="p">)</span>
<span class="n">db</span> <span class="o">=</span> <span class="n">create_database</span><span class="p">(</span><span class="n">gem_name</span><span class="p">,</span> <span class="n">gem_version</span><span class="p">)</span>
<span class="n">store_gem_spec</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">gem_name</span><span class="p">,</span> <span class="n">gem_version</span><span class="p">)</span>
<span class="n">store_docs</span><span class="p">(</span><span class="n">db</span><span class="p">)</span>
<span class="nb">puts</span> <span class="s2">"Documentation has been stored in </span><span class="si">#{</span><span class="n">gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="n">gem_version</span><span class="si">}</span><span class="s2">.db"</span>
<span class="k">end</span>
<span class="n">main</span>
</code></pre>
</div>
</div>
<p>This script will perform the following tasks:</p>
<ol>
<li>Download and parse the gem documentation using YARD
</li><li>Create a SQLite database named with the gem name and version
</li><li>Create tables for storing gem spec and documentation
</li><li>Store the gem spec (name and version) in the database
</li><li>Store the documentation in the database
</li></ol>
<p>To run the script, simply execute <code>ruby gem_doc_parser.rb</code> and follow the prompts to enter the gem name and version. The documentation will be stored in the specified SQLite database.</p>
<h2 id='lets-see-if-it-works' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#lets-see-if-it-works' aria-label='Anchor'></a>Let’s see if it works!</h2>
<p>We have GPT-4’s solution. Now its time to copy the code above into <code>gem_doc_parser.rb</code> and run it.</p>
<div class="highlight-wrapper group relative cmd">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-39bds8aj"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight cmd'><code id="code-39bds8aj">ruby gem_doc_parser.rb
</code></pre>
</div>
</div><div class="highlight-wrapper group relative output">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-ot9j7kei"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight output'><code id="code-ot9j7kei">Ignoring rbs-2.8.2 because its extensions are not built. Try: gem pristine rbs --version 2.8.2
Enter the gem name:
rake
Enter the gem version:
13.0.6
gem_doc_parser.rb:9:in `download_and_parse_gem': uninitialized constant Bundler::CLI (NameError)
Bundler::CLI.start(["gem", gem_name, "--version", gem_version])
^^^^^
from gem_doc_parser.rb:61:in `main'
from gem_doc_parser.rb:70:in `<main>'
</code></pre>
</div>
</div>
<p>Uh oh! We already run into our first error. Let’s paste that output into GPT-4 and see what it tells us.</p>
<blockquote>
<p>That didn’t work! I got this error…</p>
</blockquote>
<h3 id='gpt-4-response-2' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#gpt-4-response-2' aria-label='Anchor'></a>GPT-4 response</h3>
<p>I apologize for the confusion. I misunderstood your initial request. You want to download and parse a specific RubyGem, not create a new gem.</p>
<p>Please follow these steps to update the script:</p>
<ol>
<li>Replace the <code>download_and_parse_gem</code> method with the following:
</li></ol>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-q3ygpj7k"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-q3ygpj7k"><span class="k">def</span> <span class="nf">download_and_parse_gem</span><span class="p">(</span><span class="n">gem_name</span><span class="p">,</span> <span class="n">gem_version</span><span class="p">)</span>
<span class="n">gem_path</span> <span class="o">=</span> <span class="no">Bundler</span><span class="o">::</span><span class="no">CLI</span><span class="o">::</span><span class="no">Common</span><span class="p">.</span><span class="nf">select_spec</span><span class="p">(</span><span class="n">gem_name</span><span class="p">,</span> <span class="n">gem_version</span><span class="p">).</span><span class="nf">full_gem_path</span>
<span class="c1"># Parse the gem documentation using YARD</span>
<span class="no">YARD</span><span class="o">::</span><span class="no">Registry</span><span class="p">.</span><span class="nf">clear</span>
<span class="no">YARD</span><span class="o">::</span><span class="no">Parser</span><span class="o">::</span><span class="no">SourceParser</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">gem_path</span><span class="p">)</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<ol>
<li>Install the <code>bundler</code> gem by running:
</li></ol>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-ypzwcdmb"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-ypzwcdmb">gem install bundler
</code></pre>
</div>
</div>
<ol>
<li>Update your Gemfile to include the gem you want to download and parse:
</li></ol>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-tj3206fc"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-tj3206fc">source 'https://rubygems.org'
gem 'rake', '13.0.6'
</code></pre>
</div>
</div>
<ol>
<li>Install the specified gem with Bundler:
</li></ol>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-1gg8v5ro"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-1gg8v5ro">bundle install
</code></pre>
</div>
</div>
<p>After making these updates, you should be able to run the script with <code>ruby gem_doc_parser.rb</code>. The script will parse the documentation for the gem specified in the Gemfile and store it in a SQLite database.</p>
<h2 id='will-gpt-4s-fix-work' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#will-gpt-4s-fix-work' aria-label='Anchor'></a>Will GPT-4’s fix work?</h2>
<p>Ok, let’s do this one more time just for fun to see if it runs.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-i9gjlt6b"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-i9gjlt6b"><span class="no">Ignoring</span> <span class="n">rbs</span><span class="o">-</span><span class="mf">2.8</span><span class="o">.</span><span class="mi">2</span> <span class="n">because</span> <span class="n">its</span> <span class="n">extensions</span> <span class="n">are</span> <span class="n">not</span> <span class="n">built</span><span class="o">.</span> <span class="no">Try</span><span class="p">:</span> <span class="n">gem</span> <span class="n">pristine</span> <span class="n">rbs</span> <span class="o">--</span><span class="n">version</span> <span class="mf">2.8</span><span class="o">.</span><span class="mi">2</span>
<span class="no">Enter</span> <span class="n">the</span> <span class="n">gem</span> <span class="ss">name:
</span><span class="n">rake</span>
<span class="no">Enter</span> <span class="n">the</span> <span class="n">gem</span> <span class="ss">version:
</span><span class="mf">13.0</span><span class="o">.</span><span class="mi">6</span>
<span class="n">gem_doc_parser</span><span class="p">.</span><span class="nf">rb</span><span class="p">:</span><span class="mi">6</span><span class="ss">:in</span> <span class="sb">`download_and_parse_gem': uninitialized constant Bundler::CLI (NameError)
gem_path = Bundler::CLI::Common.select_spec(gem_name, gem_version).full_gem_path
^^^^^^^^
from gem_doc_parser.rb:58:in `</span><span class="n">main</span><span class="s1">'
from gem_doc_parser.rb:67:in `<main>'</span>
</code></pre>
</div>
</div>
<p>Nope!</p>
<p>This is where GPT-4 can be problematic for people who might be new to software development that are trying to solve specific problems right away. If you’re using it this way, you’ll find that GPT-4 will give you a plausible answer, but it usually won’t work on the first try.</p>
<p>That’s OK though, let’s get more specific about how we want this to run.</p>
<h2 id='more-experience-helps-gpt-4-give-better-results' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#more-experience-helps-gpt-4-give-better-results' aria-label='Anchor'></a>More experience helps GPT-4 give better results</h2>
<p>If you’ve built applications before, you can give GPT-4 a more specific prompt and you’ll get back an answer that’s more inline with what you want.</p>
<blockquote>
<p>That doesn’t work either. Let’s try this instead: instead of using Bundler to install the gem, just download the .gem file directly from RubyGems, save it to a temporary path, then have Yard document that one gem and dump it into a sqlite file.</p>
<p>Put all of this into one class that I can call from Ruby code. Then let’s call that class from the CLI so that looks something like this:</p>
<p>$ ruby gem-to-sqlite rake 13.0.6</p>
</blockquote>
<p>This type of prompt is something that feels more like an experienced developer talking to a developer who might just be starting out. It gives us back some more reasonable code.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-y2xljklc"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-y2xljklc"><span class="nb">require</span> <span class="s1">'yard'</span>
<span class="nb">require</span> <span class="s1">'sqlite3'</span>
<span class="nb">require</span> <span class="s1">'net/http'</span>
<span class="nb">require</span> <span class="s1">'tmpdir'</span>
<span class="nb">require</span> <span class="s1">'rubygems/package'</span>
<span class="k">class</span> <span class="nc">GemToSqlite</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">gem_name</span><span class="p">,</span> <span class="n">gem_version</span><span class="p">)</span>
<span class="vi">@gem_name</span> <span class="o">=</span> <span class="n">gem_name</span>
<span class="vi">@gem_version</span> <span class="o">=</span> <span class="n">gem_version</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">download_gem</span>
<span class="n">gem_url</span> <span class="o">=</span> <span class="s2">"https://rubygems.org/downloads/</span><span class="si">#{</span><span class="vi">@gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="vi">@gem_version</span><span class="si">}</span><span class="s2">.gem"</span>
<span class="n">uri</span> <span class="o">=</span> <span class="no">URI</span><span class="p">(</span><span class="n">gem_url</span><span class="p">)</span>
<span class="n">file_path</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="no">Dir</span><span class="p">.</span><span class="nf">tmpdir</span><span class="p">,</span> <span class="s2">"</span><span class="si">#{</span><span class="vi">@gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="vi">@gem_version</span><span class="si">}</span><span class="s2">.gem"</span><span class="p">)</span>
<span class="no">File</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">uri</span><span class="p">))</span>
<span class="n">file_path</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">extract_gem</span><span class="p">(</span><span class="n">file_path</span><span class="p">)</span>
<span class="n">spec</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="no">Gem</span><span class="o">::</span><span class="no">Package</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">file_path</span><span class="p">).</span><span class="nf">spec</span><span class="p">.</span><span class="nf">files</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">file</span><span class="o">|</span>
<span class="no">YARD</span><span class="o">::</span><span class="no">Registry</span><span class="p">.</span><span class="nf">clear</span>
<span class="no">YARD</span><span class="o">::</span><span class="no">Parser</span><span class="o">::</span><span class="no">SourceParser</span><span class="p">.</span><span class="nf">parse_string</span><span class="p">(</span><span class="n">file</span><span class="p">)</span>
<span class="n">spec</span> <span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">Package</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">file_path</span><span class="p">).</span><span class="nf">spec</span>
<span class="k">end</span>
<span class="n">spec</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">create_database</span>
<span class="n">db</span> <span class="o">=</span> <span class="no">SQLite3</span><span class="o">::</span><span class="no">Database</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="vi">@gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="vi">@gem_version</span><span class="si">}</span><span class="s2">.db"</span><span class="p">)</span>
<span class="n">db</span><span class="p">.</span><span class="nf">execute</span> <span class="o"><<-</span><span class="no">SQL</span><span class="sh">
CREATE TABLE IF NOT EXISTS gem_spec (
id INTEGER PRIMARY KEY,
name TEXT,
version TEXT
);
</span><span class="no"> SQL</span>
<span class="n">db</span><span class="p">.</span><span class="nf">execute</span> <span class="o"><<-</span><span class="no">SQL</span><span class="sh">
CREATE TABLE IF NOT EXISTS gem_docs (
id INTEGER PRIMARY KEY,
path TEXT,
type TEXT,
doc TEXT
);
</span><span class="no"> SQL</span>
<span class="n">db</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">store_gem_spec</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">spec</span><span class="p">)</span>
<span class="n">db</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="s2">"INSERT INTO gem_spec (name, version) VALUES (?, ?)"</span><span class="p">,</span> <span class="p">[</span><span class="vi">@gem_name</span><span class="p">,</span> <span class="vi">@gem_version</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">store_docs</span><span class="p">(</span><span class="n">db</span><span class="p">)</span>
<span class="no">YARD</span><span class="o">::</span><span class="no">Registry</span><span class="p">.</span><span class="nf">all</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">object</span><span class="o">|</span>
<span class="n">path</span> <span class="o">=</span> <span class="n">object</span><span class="p">.</span><span class="nf">path</span>
<span class="n">type</span> <span class="o">=</span> <span class="n">object</span><span class="p">.</span><span class="nf">type</span>
<span class="n">doc</span> <span class="o">=</span> <span class="n">object</span><span class="p">.</span><span class="nf">docstring</span><span class="p">.</span><span class="nf">to_raw</span>
<span class="n">db</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="s2">"INSERT INTO gem_docs (path, type, doc) VALUES (?, ?, ?)"</span><span class="p">,</span> <span class="p">[</span><span class="n">path</span><span class="p">,</span> <span class="n">type</span><span class="p">,</span> <span class="n">doc</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">run</span>
<span class="n">gem_path</span> <span class="o">=</span> <span class="n">download_gem</span>
<span class="n">spec</span> <span class="o">=</span> <span class="n">extract_gem</span><span class="p">(</span><span class="n">gem_path</span><span class="p">)</span>
<span class="n">db</span> <span class="o">=</span> <span class="n">create_database</span>
<span class="n">store_gem_spec</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">spec</span><span class="p">)</span>
<span class="n">store_docs</span><span class="p">(</span><span class="n">db</span><span class="p">)</span>
<span class="nb">puts</span> <span class="s2">"Documentation has been stored in </span><span class="si">#{</span><span class="vi">@gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="vi">@gem_version</span><span class="si">}</span><span class="s2">.db"</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>When I run it I get another error:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-cmfg4ni9"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-cmfg4ni9">fly-apps/gpt4 → ruby gem_to_sqlite.rb rake 13.0.6
Ignoring rbs-2.8.2 because its extensions are not built. Try: gem pristine rbs --version 2.8.2
[warn]: Syntax error in `(stdin)`:(1,8): no .<digit> floating literal anymore; put 0 before dot
[warn]: ParserSyntaxError: syntax error in `(stdin)`:(1,8): no .<digit> floating literal anymore; put 0 before dot
[warn]: Stack trace:
/Users/bradgessler/.rbenv/versions/3.2.0/lib/ruby/gems/3.2.0/gems/yard-0.9.28/lib/yard/parser/ruby/ruby_parser.rb:601:in `on_parse_error'
/Users/bradgessler/.rbenv/versions/3.2.0/lib/ruby/gems/3.2.0/gems/yard-0.9.28/lib/yard/parser/ruby/ruby_parser.rb:56:in `parse'
/Users/bradgessler/.rbenv/versions/3.2.0/lib/ruby/gems/3.2.0/gems/yard-0.9.28/lib/yard/parser/ruby/ruby_parser.rb:56:in `parse'
/Users/bradgessler/.rbenv/versions/3.2.0/lib/ruby/gems/3.2.0/gems/yard-0.9.28/lib/yard/parser/ruby/ruby_parser.rb:17:in `parse'
/Users/bradgessler/.rbenv/versions/3.2.0/lib/ruby/gems/3.2.0/gems/yard-0.9.28/lib/yard/parser/source_parser.rb:442:in `parse'
/Users/bradgessler/.rbenv/versions/3.2.0/lib/ruby/gems/3.2.0/gems/yard-0.9.28/lib/yard/parser/source_parser.rb:124:in `parse_string'
</code></pre>
</div>
</div>
<p>It gives me back this fix:</p>
<h3 id='gpt-4-response-3' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#gpt-4-response-3' aria-label='Anchor'></a>GPT-4 response</h3>
<p>Apologies for the issues you’ve encountered. It appears the error is related to the YARD parser when parsing the gem. I’ve updated the code to address this issue.</p>
<p>Update the <code>extract_gem</code> method in the <code>gem_to_sqlite.rb</code> file with the following code:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-xnjb780q"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-xnjb780q"><span class="k">def</span> <span class="nf">extract_gem</span><span class="p">(</span><span class="n">file_path</span><span class="p">)</span>
<span class="n">spec</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="n">gem_dir</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="no">Dir</span><span class="p">.</span><span class="nf">tmpdir</span><span class="p">,</span> <span class="s2">"</span><span class="si">#{</span><span class="vi">@gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="vi">@gem_version</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="c1"># Extract the gem to a temporary directory</span>
<span class="no">Gem</span><span class="o">::</span><span class="no">Package</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">file_path</span><span class="p">).</span><span class="nf">extract_files</span><span class="p">(</span><span class="n">gem_dir</span><span class="p">)</span>
<span class="n">spec</span> <span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">Package</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">file_path</span><span class="p">).</span><span class="nf">spec</span>
<span class="c1"># Parse the gem documentation using YARD</span>
<span class="no">YARD</span><span class="o">::</span><span class="no">Registry</span><span class="p">.</span><span class="nf">clear</span>
<span class="no">Dir</span><span class="p">.</span><span class="nf">glob</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="n">gem_dir</span><span class="p">,</span> <span class="s1">'**'</span><span class="p">,</span> <span class="s1">'*.rb'</span><span class="p">)).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">file</span><span class="o">|</span>
<span class="no">YARD</span><span class="o">::</span><span class="no">Parser</span><span class="o">::</span><span class="no">SourceParser</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">file</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">spec</span>
<span class="k">end</span>
</code></pre>
</div>
</div><h2 id='an-aside-inline-the-gem-bundle' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#an-aside-inline-the-gem-bundle' aria-label='Anchor'></a>An aside: inline the Gem bundle</h2>
<p>I also remembered that I want to share this code for this post, so I asked GPT-4 to move the Gems into an inline bundler.</p>
<blockquote>
<p>Could you move the require statements up top into an inline bundler block?</p>
</blockquote>
<h3 id='gpt-4-response-4' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#gpt-4-response-4' aria-label='Anchor'></a>GPT-4 response</h3>
<p>Sure! You can use an inline Bundler block to manage gem dependencies. Update the <code>gem_to_sqlite.rb</code> file with the following code:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-v3ayf07y"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-v3ayf07y">require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'yard', '~> 0.9.28'
gem 'sqlite3', '~> 1.4'
end
require 'yard'
require 'sqlite3'
require 'net/http'
require 'tmpdir'
require 'rubygems/package'
## ... (rest of the GemToSqlite class definition)
</code></pre>
</div>
</div>
<p>It’s impressive that GPT-4 is able to give me a reasonable response for an “aside”. Let’s see if that distracted it because I ran into another bug running this.</p>
<h2 id='another-bug' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#another-bug' aria-label='Anchor'></a>Another bug</h2>
<blockquote>
<p>I ran into another bug! Check it out:</p>
</blockquote>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-cfejpjvm"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-cfejpjvm">fly-apps/gpt4 → ruby gem_to_sqlite-2.rb
<span class="o">[</span>warn]: <span class="k">in </span>YARD::Handlers::Ruby::VisibilityHandler: Undocumentable statement, cannot determine method name
<span class="k">in </span>file <span class="s1">'/var/folders/_n/58_fmw2j26sc63pr7lcvcyfh0000gn/T/rake-13.0.6/lib/rake/dsl_definition.rb'</span>:23:
23: private<span class="o">(</span><span class="k">*</span>FileUtils.instance_methods<span class="o">(</span><span class="nb">false</span><span class="o">))</span>
<span class="o">[</span>warn]: <span class="k">in </span>YARD::Handlers::Ruby::VisibilityHandler: Undocumentable statement, cannot determine method name
<span class="k">in </span>file <span class="s1">'/var/folders/_n/58_fmw2j26sc63pr7lcvcyfh0000gn/T/rake-13.0.6/lib/rake/dsl_definition.rb'</span>:24:
24: private<span class="o">(</span><span class="k">*</span>FileUtilsExt.instance_methods<span class="o">(</span><span class="nb">false</span><span class="o">))</span>
/Users/bradgessler/.rbenv/versions/3.2.0/lib/ruby/gems/3.2.0/gems/sqlite3-1.6.1-arm64-darwin/lib/sqlite3/statement.rb:41:in <span class="sb">`</span>bind_param<span class="s1">': can'</span>t prepare Symbol <span class="o">(</span>RuntimeError<span class="o">)</span>
from /Users/bradgessler/.rbenv/versions/3.2.0/lib/ruby/gems/3.2.0/gems/sqlite3-1.6.1-arm64-darwin/lib/sqlite3/statement.rb:41:in <span class="sb">`</span>block <span class="k">in </span>bind_params<span class="s1">'
</span></code></pre>
</div>
</div><h3 id='gpt-4-response-5' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#gpt-4-response-5' aria-label='Anchor'></a>GPT-4 response</h3>
<p>I apologize for the oversight. It seems that the error is related to the insertion of data into the SQLite database. The issue comes from the YARD object type being a Symbol, which SQLite doesn’t support. To fix this, we need to convert the object type to a String before inserting it into the database.</p>
<p>Update the <code>store_docs</code> method in the <code>gem_to_sqlite.rb</code> file with the following code:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-oewc672q"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-oewc672q">def store_docs(db)
YARD::Registry.all.each do |object|
path = object.path
type = object.type.to_s
doc = object.docstring.to_raw
db.execute("INSERT INTO gem_docs (path, type, doc) VALUES (?, ?, ?)", [path, type, doc])
end
end
</code></pre>
</div>
</div>
<p>The only change made is converting the object type to a string using <code>to_s</code>:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-42g4depq"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-42g4depq">type = object.type.to_s
</code></pre>
</div>
</div>
<p>With this change, you should be able to run the script without any issues:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-y6gtw1xh"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-y6gtw1xh">./gem-to-sqlite rake 13.0.6
</code></pre>
</div>
</div>
<p>Let’s try it again!</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-hkeguiao"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-hkeguiao">fly-apps/gpt4 → ruby gem_to_sqlite-2.rb
<span class="o">[</span>warn]: <span class="k">in </span>YARD::Handlers::Ruby::VisibilityHandler: Undocumentable statement, cannot determine method name
<span class="k">in </span>file <span class="s1">'/var/folders/_n/58_fmw2j26sc63pr7lcvcyfh0000gn/T/rake-13.0.6/lib/rake/dsl_definition.rb'</span>:23:
23: private<span class="o">(</span><span class="k">*</span>FileUtils.instance_methods<span class="o">(</span><span class="nb">false</span><span class="o">))</span>
<span class="o">[</span>warn]: <span class="k">in </span>YARD::Handlers::Ruby::VisibilityHandler: Undocumentable statement, cannot determine method name
<span class="k">in </span>file <span class="s1">'/var/folders/_n/58_fmw2j26sc63pr7lcvcyfh0000gn/T/rake-13.0.6/lib/rake/dsl_definition.rb'</span>:24:
24: private<span class="o">(</span><span class="k">*</span>FileUtilsExt.instance_methods<span class="o">(</span><span class="nb">false</span><span class="o">))</span>
Documentation has been stored <span class="k">in </span>rake-13.0.6.db
</code></pre>
</div>
</div>
<p>Success! Let’s open up the sqlite database and make sure it has what we expect.</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-st6c0xna"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-st6c0xna">sqlite> select * from gem_spec;
1|rake|13.0.6
sqlite> select * from gem_docs;
1|RDoc|module|
2|RDoc::Page|module|
3|RDoc::Page::FONTS|constant|
4|RDoc::Page::STYLE|constant|
5|RDoc::Page::XHTML_PREAMBLE|constant|
6|RDoc::Page::HEADER|constant|
7|RDoc::Page::FILE_PAGE|constant|
8|RDoc::Page::CLASS_PAGE|constant|
9|RDoc::Page::METHOD_LIST|constant|
10|RDoc::Page::FOOTER|constant|
11|RDoc::Page::BODY|constant|
12|RDoc::Page::SRC_PAGE|constant|Source code ##########################
13|RDoc::Page::FR_INDEX_BODY|constant|Index ################################
14|RDoc::Page::FILE_INDEX|constant|
15|RDoc::Page::CLASS_INDEX|constant|
16|RDoc::Page::METHOD_INDEX|constant|
17|RDoc::Page::INDEX|constant|
18|Rake|module|--
Copyright 2003-2010 by Jim Weirich (jim.weirich@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
++
19|Rake::CommandLineOptionError|constant|
</code></pre>
</div>
</div>
<p>Not bad! We can see our gem name and version in the <code>gem_spec</code> table and all of the docs in <code>gem_docs</code>.</p>
<h2 id='lets-have-gpt-4-clean-this-up-for-a-demo' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#lets-have-gpt-4-clean-this-up-for-a-demo' aria-label='Anchor'></a>Let’s have GPT-4 clean this up for a demo</h2>
<p>Since I need to bundle all of this up in a way that I can share it, I’m going to ask GPT-4 to do the work.</p>
<blockquote>
<p>It works! I need to get all of this into one file so that I can share it via a Github Gist. Could you consolidate into one file, remove any unnecessary code, and include in code comments up top how to run the script? Make sure its a <code>.rb</code> file and let’s tell people to run it via <code>ruby gem-to-sqlite.rb rake 13.0.6</code> (for example)</p>
</blockquote>
<h3 id='gpt-4-response-6' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#gpt-4-response-6' aria-label='Anchor'></a>GPT-4 response</h3>
<p>Sure! Here’s the consolidated version of the script as a single file named <code>gem-to-sqlite.rb</code>. I’ve also added comments at the beginning of the file with instructions on how to run it.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-unfhliew"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-unfhliew"><span class="c1"># gem-to-sqlite.rb</span>
<span class="c1">#</span>
<span class="c1"># Usage: ruby gem-to-sqlite.rb <gem_name> <gem_version></span>
<span class="c1"># Example: ruby gem-to-sqlite.rb rake 13.0.6</span>
<span class="nb">require</span> <span class="s1">'bundler/inline'</span>
<span class="n">gemfile</span> <span class="k">do</span>
<span class="n">source</span> <span class="s1">'https://rubygems.org'</span>
<span class="n">gem</span> <span class="s1">'yard'</span><span class="p">,</span> <span class="s1">'~> 0.9.28'</span>
<span class="n">gem</span> <span class="s1">'sqlite3'</span><span class="p">,</span> <span class="s1">'~> 1.4'</span>
<span class="k">end</span>
<span class="nb">require</span> <span class="s1">'yard'</span>
<span class="nb">require</span> <span class="s1">'sqlite3'</span>
<span class="nb">require</span> <span class="s1">'net/http'</span>
<span class="nb">require</span> <span class="s1">'tmpdir'</span>
<span class="nb">require</span> <span class="s1">'rubygems/package'</span>
<span class="k">class</span> <span class="nc">GemToSqlite</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">gem_name</span><span class="p">,</span> <span class="n">gem_version</span><span class="p">)</span>
<span class="vi">@gem_name</span> <span class="o">=</span> <span class="n">gem_name</span>
<span class="vi">@gem_version</span> <span class="o">=</span> <span class="n">gem_version</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">download_gem</span>
<span class="n">gem_url</span> <span class="o">=</span> <span class="s2">"https://rubygems.org/downloads/</span><span class="si">#{</span><span class="vi">@gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="vi">@gem_version</span><span class="si">}</span><span class="s2">.gem"</span>
<span class="n">uri</span> <span class="o">=</span> <span class="no">URI</span><span class="p">(</span><span class="n">gem_url</span><span class="p">)</span>
<span class="n">file_path</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="no">Dir</span><span class="p">.</span><span class="nf">tmpdir</span><span class="p">,</span> <span class="s2">"</span><span class="si">#{</span><span class="vi">@gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="vi">@gem_version</span><span class="si">}</span><span class="s2">.gem"</span><span class="p">)</span>
<span class="no">File</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">uri</span><span class="p">))</span>
<span class="n">file_path</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">extract_gem</span><span class="p">(</span><span class="n">file_path</span><span class="p">)</span>
<span class="n">spec</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="n">gem_dir</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="no">Dir</span><span class="p">.</span><span class="nf">tmpdir</span><span class="p">,</span> <span class="s2">"</span><span class="si">#{</span><span class="vi">@gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="vi">@gem_version</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="c1"># Extract the gem to a temporary directory</span>
<span class="no">Gem</span><span class="o">::</span><span class="no">Package</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">file_path</span><span class="p">).</span><span class="nf">extract_files</span><span class="p">(</span><span class="n">gem_dir</span><span class="p">)</span>
<span class="n">spec</span> <span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">Package</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">file_path</span><span class="p">).</span><span class="nf">spec</span>
<span class="c1"># Parse the gem documentation using YARD</span>
<span class="no">YARD</span><span class="o">::</span><span class="no">Registry</span><span class="p">.</span><span class="nf">clear</span>
<span class="no">Dir</span><span class="p">.</span><span class="nf">glob</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="n">gem_dir</span><span class="p">,</span> <span class="s1">'**'</span><span class="p">,</span> <span class="s1">'*.rb'</span><span class="p">)).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">file</span><span class="o">|</span>
<span class="no">YARD</span><span class="o">::</span><span class="no">Parser</span><span class="o">::</span><span class="no">SourceParser</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">file</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">spec</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">create_database</span>
<span class="n">db</span> <span class="o">=</span> <span class="no">SQLite3</span><span class="o">::</span><span class="no">Database</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="vi">@gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="vi">@gem_version</span><span class="si">}</span><span class="s2">.db"</span><span class="p">)</span>
<span class="n">db</span><span class="p">.</span><span class="nf">execute</span> <span class="o"><<-</span><span class="no">SQL</span><span class="sh">
CREATE TABLE IF NOT EXISTS gem_spec (
id INTEGER PRIMARY KEY,
name TEXT,
version TEXT
);
</span><span class="no"> SQL</span>
<span class="n">db</span><span class="p">.</span><span class="nf">execute</span> <span class="o"><<-</span><span class="no">SQL</span><span class="sh">
CREATE TABLE IF NOT EXISTS gem_docs (
id INTEGER PRIMARY KEY,
path TEXT,
type TEXT,
doc TEXT
);
</span><span class="no"> SQL</span>
<span class="n">db</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">store_gem_spec</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">spec</span><span class="p">)</span>
<span class="n">db</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="s2">"INSERT INTO gem_spec (name, version) VALUES (?, ?)"</span><span class="p">,</span> <span class="p">[</span><span class="vi">@gem_name</span><span class="p">,</span> <span class="vi">@gem_version</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">store_docs</span><span class="p">(</span><span class="n">db</span><span class="p">)</span>
<span class="no">YARD</span><span class="o">::</span><span class="no">Registry</span><span class="p">.</span><span class="nf">all</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">object</span><span class="o">|</span>
<span class="n">path</span> <span class="o">=</span> <span class="n">object</span><span class="p">.</span><span class="nf">path</span>
<span class="n">type</span> <span class="o">=</span> <span class="n">object</span><span class="p">.</span><span class="nf">type</span><span class="p">.</span><span class="nf">to_s</span>
<span class="n">doc</span> <span class="o">=</span> <span class="n">object</span><span class="p">.</span><span class="nf">docstring</span><span class="p">.</span><span class="nf">to_raw</span>
<span class="n">db</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="s2">"INSERT INTO gem_docs (path, type, doc) VALUES (?, ?, ?)"</span><span class="p">,</span> <span class="p">[</span><span class="n">path</span><span class="p">,</span> <span class="n">type</span><span class="p">,</span> <span class="n">doc</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">run</span>
<span class="n">gem_path</span> <span class="o">=</span> <span class="n">download_gem</span>
<span class="n">spec</span> <span class="o">=</span> <span class="n">extract_gem</span><span class="p">(</span><span class="n">gem_path</span><span class="p">)</span>
<span class="n">db</span> <span class="o">=</span> <span class="n">create_database</span>
<span class="n">store_gem_spec</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">spec</span><span class="p">)</span>
<span class="n">store_docs</span><span class="p">(</span><span class="n">db</span><span class="p">)</span>
<span class="nb">puts</span> <span class="s2">"Documentation has been stored in </span><span class="si">#{</span><span class="vi">@gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="vi">@gem_version</span><span class="si">}</span><span class="s2">.db"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">if</span> <span class="no">ARGV</span><span class="p">.</span><span class="nf">length</span> <span class="o">!=</span> <span class="mi">2</span>
<span class="nb">puts</span> <span class="s2">"Usage: ruby gem-to-sqlite.rb <gem_name> <gem_version>"</span>
<span class="nb">exit</span> <span class="mi">1</span>
<span class="k">end</span>
<span class="n">gem_name</span> <span class="o">=</span> <span class="no">ARGV</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">gem_version</span> <span class="o">=</span> <span class="no">ARGV</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="n">gem_to_sqlite</span> <span class="o">=</span> <span class="no">GemToSqlite</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">gem_name</span><span class="p">,</span> <span class="n">gem_version</span><span class="p">)</span>
<span class="n">gem_to_sqlite</span><span class="p">.</span><span class="nf">run</span>
</code></pre>
</div>
</div>
<p>That’s pretty darn good, and the <a href='https://gist.github.com/bradgessler/02b201c1cc94c24836195db762ada02d' title=''>gist is available at Github</a>.</p>
<h2 id='gpt-4-likes-to-fake-it-until-it-makes-it-or-you-lose-your-patience' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#gpt-4-likes-to-fake-it-until-it-makes-it-or-you-lose-your-patience' aria-label='Anchor'></a>GPT-4 likes to fake it until it makes it (or you lose your patience)</h2>
<p>Now I’m going to ask it a question that will probably break it using questionably imprecise language:</p>
<blockquote>
<p>How could I use an instance of <code>YARD::Registry</code> instead of the <code>.all</code> singleton?</p>
</blockquote>
<h3 id='gpt-4-response-7' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#gpt-4-response-7' aria-label='Anchor'></a>GPT-4 response</h3>
<p>You can create a new instance of <code>YARD::Registry</code> and use it to parse and store the documentation. First, create an instance variable <code>@registry</code> in the <code>initialize</code> method:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-5kclniil"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-5kclniil"><span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">gem_name</span><span class="p">,</span> <span class="n">gem_version</span><span class="p">)</span>
<span class="vi">@gem_name</span> <span class="o">=</span> <span class="n">gem_name</span>
<span class="vi">@gem_version</span> <span class="o">=</span> <span class="n">gem_version</span>
<span class="vi">@registry</span> <span class="o">=</span> <span class="no">YARD</span><span class="o">::</span><span class="no">Registry</span><span class="p">.</span><span class="nf">new</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<hr>
<p>Bzzt wrong! <code>YARD::Registry</code> is a module. GPT-4 doesn’t necessarily know what the actual Ruby code is behind what it’s telling you—it seems to be matching patterns on something that seems like it should be a class, but it’s not.</p>
<h2 id='lets-make-a-version-for-squirrels' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#lets-make-a-version-for-squirrels' aria-label='Anchor'></a>Let’s make a version for squirrels 🐿️</h2>
<p>Time to get silly! We made this script for squirrels, so let’s ask GPT to rename a few things to make it more understandable to squirrels.</p>
<blockquote>
<p>Let’s rename everything to something that would make a squirrel really happy</p>
</blockquote>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-aadxedor"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-aadxedor"><span class="c1"># squirrel_gem_nut_collector.rb</span>
<span class="c1">#</span>
<span class="c1"># Usage: ruby squirrel_gem_nut_collector.rb <gem_name> <gem_version></span>
<span class="c1"># Example: ruby squirrel_gem_nut_collector.rb rake 13.0.6</span>
<span class="nb">require</span> <span class="s1">'bundler/inline'</span>
<span class="n">gemfile</span> <span class="k">do</span>
<span class="n">source</span> <span class="s1">'https://rubygems.org'</span>
<span class="n">gem</span> <span class="s1">'yard'</span><span class="p">,</span> <span class="s1">'~> 0.9.28'</span>
<span class="n">gem</span> <span class="s1">'sqlite3'</span><span class="p">,</span> <span class="s1">'~> 1.4'</span>
<span class="k">end</span>
<span class="nb">require</span> <span class="s1">'yard'</span>
<span class="nb">require</span> <span class="s1">'sqlite3'</span>
<span class="nb">require</span> <span class="s1">'net/http'</span>
<span class="nb">require</span> <span class="s1">'tmpdir'</span>
<span class="nb">require</span> <span class="s1">'rubygems/package'</span>
<span class="k">class</span> <span class="nc">SquirrelGemNutCollector</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">nutty_gem_name</span><span class="p">,</span> <span class="n">crunchy_gem_version</span><span class="p">)</span>
<span class="vi">@nutty_gem_name</span> <span class="o">=</span> <span class="n">nutty_gem_name</span>
<span class="vi">@crunchy_gem_version</span> <span class="o">=</span> <span class="n">crunchy_gem_version</span>
<span class="vi">@nut_registry</span> <span class="o">=</span> <span class="no">YARD</span><span class="o">::</span><span class="no">Registry</span><span class="p">.</span><span class="nf">new</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">nut_fetcher</span>
<span class="n">nut_url</span> <span class="o">=</span> <span class="s2">"https://rubygems.org/downloads/</span><span class="si">#{</span><span class="vi">@nutty_gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="vi">@crunchy_gem_version</span><span class="si">}</span><span class="s2">.gem"</span>
<span class="n">uri</span> <span class="o">=</span> <span class="no">URI</span><span class="p">(</span><span class="n">nut_url</span><span class="p">)</span>
<span class="n">temp_nut_path</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="no">Dir</span><span class="p">.</span><span class="nf">tmpdir</span><span class="p">,</span> <span class="s2">"</span><span class="si">#{</span><span class="vi">@nutty_gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="vi">@crunchy_gem_version</span><span class="si">}</span><span class="s2">.gem"</span><span class="p">)</span>
<span class="no">File</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">temp_nut_path</span><span class="p">,</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">uri</span><span class="p">))</span>
<span class="n">temp_nut_path</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">crack_nut</span><span class="p">(</span><span class="n">file_path</span><span class="p">)</span>
<span class="n">nut_spec</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="n">cracked_nut_dir</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="no">Dir</span><span class="p">.</span><span class="nf">tmpdir</span><span class="p">,</span> <span class="s2">"</span><span class="si">#{</span><span class="vi">@nutty_gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="vi">@crunchy_gem_version</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="c1"># Crack open the gem nut and release the code</span>
<span class="no">Gem</span><span class="o">::</span><span class="no">Package</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">file_path</span><span class="p">).</span><span class="nf">extract_files</span><span class="p">(</span><span class="n">cracked_nut_dir</span><span class="p">)</span>
<span class="n">nut_spec</span> <span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">Package</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">file_path</span><span class="p">).</span><span class="nf">spec</span>
<span class="c1"># Parse the code with the power of YARD</span>
<span class="vi">@nut_registry</span><span class="p">.</span><span class="nf">clear</span>
<span class="no">Dir</span><span class="p">.</span><span class="nf">glob</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="n">cracked_nut_dir</span><span class="p">,</span> <span class="s1">'**'</span><span class="p">,</span> <span class="s1">'*.rb'</span><span class="p">)).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">file</span><span class="o">|</span>
<span class="no">YARD</span><span class="o">::</span><span class="no">Parser</span><span class="o">::</span><span class="no">SourceParser</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="vi">@nut_registry</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">nut_spec</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">nut_storage</span>
<span class="n">nut_db</span> <span class="o">=</span> <span class="no">SQLite3</span><span class="o">::</span><span class="no">Database</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="vi">@nutty_gem_name</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="vi">@crunchy_gem_version</span><span class="si">}</span><span class="s2">.db"</span><span class="p">)</span>
<span class="n">nut_db</span><span class="p">.</span><span class="nf">execute</span> <span class="o"><<-</span><span class="no">SQL</span><span class="sh">
CREATE TABLE IF NOT EXISTS nut_specs (
id INTEGER PRIMARY KEY,
name TEXT,
version TEXT
);
</span><span class="no"> SQL</span>
<span class="n">nut_db</span><span class="p">.</span><span class="nf">execute</span> <span class="o"><<-</span><span class="no">SQL</span><span class="sh">
CREATE TABLE IF NOT EXISTS nut_docs (
id INTEGER PRIMARY KEY,
path TEXT,
type TEXT,
doc TEXT
);
</span><span class="no"> SQL</span>
<span class="n">nut_db</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">store_nut_specs</span><span class="p">(</span><span class="n">nut_db</span><span class="p">,</span> <span class="n">spec</span><span class="p">)</span>
<span class="n">nut_db</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="s2">"INSERT INTO nut_specs (name, version) VALUES (?, ?)"</span><span class="p">,</span> <span class="p">[</span><span class="vi">@nutty_gem_name</span><span class="p">,</span> <span class="vi">@crunchy_gem_version</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">fill_nut_storage_with_magic</span><span class="p">(</span><span class="n">nut_db</span><span class="p">)</span>
<span class="vi">@nut_registry</span><span class="p">.</span><span class="nf">all</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">object</span><span class="o">|</span>
<span class="n">nutty_path</span> <span class="o">=</span> <span class="n">object</span><span class="p">.</span><span class="nf">path</span>
<span class="n">crunchy_type</span> <span class="o">=</span> <span class="n">object</span><span class="p">.</span><span class="nf">type</span><span class="p">.</span><span class="nf">to_s</span>
<span class="n">docs_of_wisdom</span> <span class="o">=</span> <span class="n">object</span><span class="p">.</span><span class="nf">docstring</span><span class="p">.</span><span class="nf">to_raw</span>
<span class="n">nut_db</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="s2">"INSERT INTO nut_docs (path, type, doc) VALUES (?, ?, ?)"</span><span class="p">,</span> <span class="p">[</span><span class="n">nutty_path</span><span class="p">,</span> <span class="n">crunchy_type</span><span class="p">,</span> <span class="n">docs_of_wisdom</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">collect_all_the_nuts</span>
<span class="n">path_to_nut</span> <span class="o">=</span> <span class="n">nut_fetcher</span>
<span class="n">nut_spec</span> <span class="o">=</span> <span class="n">crack_nut</span><span class="p">(</span><span class="n">path_to_nut</span><span class="p">)</span>
<span class="n">nut_db</span> <span class="o">=</span> <span class="n">nut_storage</span>
<span class="n">store_nut_specs</span><span class="p">(</span><span class="n">nut_db</span><span class="p">,</span> <span class="n">nut_spec</span><span class="p">)</span>
<span class="n">fill_nut_storage_with_magic</span><span class="p">(</span><span class="n">nut_db</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">if</span> <span class="no">ARGV</span><span class="p">.</span><span class="nf">size</span>
</code></pre>
</div>
</div>
<p>😆</p>
<p>I’ll spare you these prompts, but I can assure you they were hilarious.</p>
<ul>
<li>How about for a old timey prospector from the gold rush?
</li><li>Could you give me a cool hip hop version?
</li><li>Do one for using words a 4 year old would understand
</li></ul>
<h2 id='overall-gpt-4-is-a-useful-tool' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#overall-gpt-4-is-a-useful-tool' aria-label='Anchor'></a>Overall GPT-4 is a useful tool</h2>
<p>To sum it up, here’s what you should keep in mind when you add GPT-4 to your set of tools:</p>
<h3 id='you-still-need-to-know-what-youre-doing' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#you-still-need-to-know-what-youre-doing' aria-label='Anchor'></a>You still need to know what you’re doing</h3>
<p>If you’re a senior developer, giving GPT-4 more specific prompts results in better responses. It can reason with you through architectural problems or write you code samples; however, many times it doesn’t know the right answer but confidently tells you something anyway.</p>
<p>If you’re a beginner, GPT-4 will give you responses that you might be able to fix and get running, but it’s not the most concise way of doing it. Often times though the code won’t work and you might not know how to ask it to fix it.</p>
<h3 id='its-great-at-jumping-contexts' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#its-great-at-jumping-contexts' aria-label='Anchor'></a>It’s great at jumping contexts</h3>
<p>GPT-4 shines when spanning different contexts. If comfortable in one programming language or framework and need to learn a new one, GPT-4 can help explain it to you and relate it back to concepts that you’re accustomed to. In a lot of ways, GPT-4 is like looking into a mirror.</p>
<h3 id='it-makes-up-seemingly-plausible-answers-that-could-be-wrong' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#it-makes-up-seemingly-plausible-answers-that-could-be-wrong' aria-label='Anchor'></a>It makes up seemingly plausible answers that could be wrong.</h3>
<p>When I asked GPT-4 how to create an instance of a <code>YARD::Registry</code>, instead of using the singleton, it told me <code>YARD::Registry.new</code> . That seems right when thinking about the structure of idiomatic Ruby code, but YARD is not, so GPT-4 has a harder time answering the question.</p>
<h3 id='it-gets-stuck-when-the-quot-contexts-quot-get-too-large-like-if-youre-asking-for-code-from-5-different-files' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#it-gets-stuck-when-the-quot-contexts-quot-get-too-large-like-if-youre-asking-for-code-from-5-different-files' aria-label='Anchor'></a>It gets stuck when the “contexts” get too large, like if you’re asking for code from 5 different files</h3>
<p>If you give GPT-4 too broad of a problem to solve, it can get lost and forget things. If you ask it to solve a problem for you like “how do I create server-side anchor tags using Turbo and Hotwire”, it will give you an initial answer that seems right across 5+ files. When you start fixing errors in those files, GPT-4 will loose track of some of the files and start giving you suggestions for fixing completely different code.</p>
/ruby-dispatch/pairing-with-gpt-4/assets/gpt-4-thumbnail.webpMRSK vs Fly.iohttps://fly.io/ruby-dispatch/mrsk-vs-flyio/2023-03-16T00:00:00+00:002023-11-21T21:08:38+00:00<p><a href='https://world.hey.com/dhh/introducing-mrsk-9330a267' title=''>MRSK</a> was introduced last month and it truly is a game changer. From the announcement:</p>
<blockquote>
<p>It sits on top of basic Docker, and harvests all the benefits you get from isolated containers with a sliver of the complexity associated with most other solutions. Instead of sending the deployment pipeline off to servers in the cloud, it runs entirely on your own machine. Just like Capistrano did.</p>
</blockquote>
<p>And it appears that the admiration is mutual:</p>
<p><a href='https://twitter.com/dhh/status/1632044101418745864' title=''><img alt="DHH Praise Tweet" src="/images/dhh-praise-tweet.webp?card&border" /></a></p>
<p>Aw, shucks. Makes me want to blush. I suspect that much of the admiration is due to the fact that <a href='https://fly.io/' title=''>fly.io</a> shares more in common with your local data-center than with most cloud providers. <a href='https://fly.io/blog/the-serverless-server/' title=''>We transmogrify Docker containers into lightweight micro-VMs and run them on our own hardware in racks around the world, so your apps can run close to your users</a>. It is also worth noting that the back-end for our GraphQL API is a <a href='https://fly.io/docs/hiring/stack/' title=''>Rails app</a>.</p>
<hr>
<p>While there are plenty of differences between MRSK and fly.io, the starting and end points are pretty much the same.</p>
<p>You start with an application and a Dockerfile. Rails 7.1 will <a href='https://fly.io/ruby-dispatch/rails-on-docker/' title=''>provide a starter Dockerfile</a>. <code>fly launch</code> will use this Dockerfile if it exists, otherwise it will provide Dockerfiles for all current versions of Rails as well as a number of other frameworks including Elixir, Laravel, Django, a number of Node frameworks, and even Rack apps. We even make <a href='https://github.com/rubys/dockerfile-rails' title=''>dockerfile-rails</a> available to all - you are welcome to use it to build Dockerfiles for your existing Rails applications and deploy to any datacenter or cloud using MRSK.</p>
<p>The desired result is to have multiple instances of your application deployed, each connected to a common database, load balanced, protected by a firewall, with a SSL certificate, and monitored with <a href='https://fly.io/ruby-dispatch/health-checks/' title=''>health checks</a>.</p>
<hr>
<p>DHH followed up with a video demonstrating the workflow using MRSK:</p>
<div class="youtube-container" data-exclude-render>
<div class="youtube-video">
<iframe
width="100%"
height="100%"
src="https://www.youtube.com/embed/LL1cV2FXZ5I"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
</div>
</div>
<p>Following are key points in the video:</p>
<div class='group relative min-w-0 bg-white shadow-md shadow-navy-500/10 rounded-xl mb-7 ring-1 ring-navy-300/40'><button type='button' class='bubble-wrap z-20 absolute right-2.5 top-2.5 text-transparent group-hover:text-navy-950 hocus:text-violet-600 bg-transparent group-hover:bg-white hocus:bg-violet-200/40 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none' data-wrap-target='#table-yrnzdri8' data-wrap-type='nowrap'><svg class='w-5 h-5 pointer-events-none' viewBox='0 0 20 20' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><g buffered-rendering='static'><path d='M11.912 10.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.314 2.314 0 00-2.315-2.31H4.959M15.187 14.5H4.959M8.802 10H4.959' /><path d='M13.081 8.466l-1.548 1.571 1.548 1.571' /></g></svg><span class='bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950'>Wrap text</span></button><div class='min-w-0 overflow-x-auto rounded-xl'><table class='table-stripe table-stretch table-pad text-sm whitespace-nowrap m-0' id='table-yrnzdri8'><thead class='text-navy-950 text-left'><tr>
<th>Time</th>
<th>Description</th>
</tr>
</thead><tbody><tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=134' title=''>2:15</a></td>
<td>MRSK requires a bit of knowledge of Linux and Docker</td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=174' title=''>2:55</a></td>
<td><code>rails new ship --main --css tailwind</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=181' title=''>3:02</a></td>
<td><code>cd ship</code></td>
</tr>
<tr>
<td></td>
<td><code>rails g scaffold post title:string body:text</code></td>
</tr>
<tr>
<td></td>
<td><code>rails db:migrate</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=218' title=''>3:39</a></td>
<td>Hetzner - select region</td>
</tr>
<tr>
<td></td>
<td>Create server</td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=251' title=''>4:12</a></td>
<td>create <code>config/deploy.yml</code></td>
</tr>
<tr>
<td></td>
<td>specify the service name, image name</td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=259' title=''>4:20</a></td>
<td>grab the ip address, paste into deploy.yml</td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=267' title=''>4:28</a></td>
<td>set up an environment template: mrsk envify</td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=283' title=''>4:44</a></td>
<td><code>mrsk deploy</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=390' title=''>6:31</a></td>
<td>note that docker may need to be restarted;</td>
</tr>
<tr>
<td></td>
<td>recommend remote docker buildx setup</td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=434' title=''>7:15</a></td>
<td>create another vm for db and app servers</td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=451' title=''>7:32</a></td>
<td>add two firewalls - ports 80 and 22 for the app servers, 3306 for db</td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=507' title=''>8:28</a></td>
<td>add load balancer</td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=534' title=''>8:55</a></td>
<td>change rails db config, install gem, change <code>config/deploy.yml</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=579' title=''>9:40</a></td>
<td>create database</td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=608' title=''>10:09</a></td>
<td>update <code>config/database.yml</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=622' title=''>10:23</a></td>
<td>update env template with mysql root password</td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=643' title=''>10:44</a></td>
<td><code>mrsk setup</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=679' title=''>11:20</a></td>
<td><code>mrsk redploy</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=697' title=''>11:38</a></td>
<td><code>mrsk details</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=758' title=''>12:39</a></td>
<td><code>mrsk rollback</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=790' title=''>13:11</a></td>
<td>setup domain name using Cloudflare</td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=827' title=''>13:48</a></td>
<td>http3 and compression</td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=861' title=''>14:22</a></td>
<td>failure - look at logs with nice grep feature!</td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=883' title=''>14:44</a></td>
<td>origin mismatch</td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=905' title=''>15:06</a></td>
<td>uncomment two lines in <code>config/environments/production.rb</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=LL1cV2FXZ5I&t=927' title=''>15:28</a></td>
<td><code>mrsk redeploy</code></td>
</tr>
</tbody></table></div></div>
<p>Inspired by DHH’s video, I made one of my own, developing and deploying the same application. I substituted fly.io for Hetzner. Rails 7.0.4.3 for Rails 7.1.0.alpha. And Postgresql for MySQL.</p>
<div class="youtube-container" data-exclude-render>
<div class="youtube-video">
<iframe
width="100%"
height="100%"
src="https://www.youtube.com/embed/eHWPx04J1OY"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
</div>
</div>
<p>Here are the key points in the video:</p>
<div class='group relative min-w-0 bg-white shadow-md shadow-navy-500/10 rounded-xl mb-7 ring-1 ring-navy-300/40'><button type='button' class='bubble-wrap z-20 absolute right-2.5 top-2.5 text-transparent group-hover:text-navy-950 hocus:text-violet-600 bg-transparent group-hover:bg-white hocus:bg-violet-200/40 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none' data-wrap-target='#table-d7op0o8r' data-wrap-type='nowrap'><svg class='w-5 h-5 pointer-events-none' viewBox='0 0 20 20' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><g buffered-rendering='static'><path d='M11.912 10.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.314 2.314 0 00-2.315-2.31H4.959M15.187 14.5H4.959M8.802 10H4.959' /><path d='M13.081 8.466l-1.548 1.571 1.548 1.571' /></g></svg><span class='bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950'>Wrap text</span></button><div class='min-w-0 overflow-x-auto rounded-xl'><table class='table-stripe table-stretch table-pad text-sm whitespace-nowrap m-0' id='table-d7op0o8r'><thead class='text-navy-950 text-left'><tr>
<th>Time</th>
<th>Description</th>
</tr>
</thead><tbody><tr>
<td><a href='https://www.youtube.com/watch?v=eHWPx04J1OY&t=80' title=''>1:21</a></td>
<td><code>rails new ship --css tailwind</code></td>
</tr>
<tr>
<td></td>
<td><code>cd ship</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=eHWPx04J1OY&t=109' title=''>1:50</a></td>
<td><code>rails g scaffold post title:string body:text</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=eHWPx04J1OY&t=113' title=''>1:54</a></td>
<td><code>flyctl launch --force-machines --region ord</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=eHWPx04J1OY&t=187' title=''>3:08</a></td>
<td><code>fly deploy</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=eHWPx04J1OY&t=269' title=''>4:30</a></td>
<td><code>fly open</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=eHWPx04J1OY&t=301' title=''>5:02</a></td>
<td><code>fly machine clone --region cdg</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=eHWPx04J1OY&t=347' title=''>5:48</a></td>
<td><code>fly dashboard</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=eHWPx04J1OY&t=444' title=''>7:25</a></td>
<td><code>vi app/views/posts/index.html.erb</code></td>
</tr>
<tr>
<td><a href='https://www.youtube.com/watch?v=eHWPx04J1OY&t=461' title=''>7:42</a></td>
<td><code>fly deploy</code></td>
</tr>
</tbody></table></div></div><figure class="post-cta">
<figcaption>
<h1>You can play with this right now.</h1>
<p>I used the [Fly.io terminal](https://fly.io/terminal) for this demo - try it yourself if you have a GitHub account or an email address. You can also run this on your own machine, we have a [Hands-on](https://fly.io/docs/hands-on/) guide that will walk you through the steps.</p>
<a class="button bg-white text-violet-500 hover:bg-opacity-75 hover:text-violet-700 transition-colors" href="https://fly.io/docs/rails/">
Try Fly for free <span class='opacity-50'>→</span>
</a>
</figcaption>
<div class="image-container">
<img src="/static/images/cta-cat.webp" srcset="/static/images/cta-cat@2x.webp 2x" alt="">
</div>
</figure>
<p>Like DHH, I started with a cold cache - in particular with no Docker layers already having previously been built.
Unlike DHH, I chose <strong class='font-[550] text-navy-950'>NOT</strong> to fast forward over the boring parts, as I want
to realistically depict how long it takes to deploy an application.</p>
<hr>
<p>Both Fly.io and MRSK are young, and undoubtedly will learn much from each other. I am particularly envious of rollback, love the log grep feature, and while Cloudflare has moved on to HTTP/3, Fly.io is currently only at HTTP/2.</p>
<p>As both MRSK and fly.io share the goal of providing to the developer only a <em>sliver of the complexity associated with most other solutions</em>,
the true winners of these types of competition are developers everywhere.</p>
<p>By standardizing on a common piece of infrastructure, in this case Dockerfiles,
people are free to pick the solution that best suits their needs, confident in
the fact that they can switch back and forth or even use both simultaneously as
there is no lock in here.</p>
<p>We welcome you to compare our
<a href='https://fly.io/docs/about/pricing/' title=''>prices</a> against your favorite data center
or cloud, and to draw your own conclusions as to matters like developer
ergonomics and ease of use.</p>
/ruby-dispatch/mrsk-vs-flyio/assets/mrsk-vs-flyio-thumbnail.webpLittle Gestures of Confidence Make Our Tools & Community Betterhttps://fly.io/ruby-dispatch/little-gestures-of-confidence/2023-03-09T00:00:00+00:002023-11-21T21:08:38+00:00<p><strong class='font-[550] text-navy-950'>The world is a much better place when we say “please” and “thanks” as we go about our daily lives. The same should be true for the tools, frameworks, documentation, and code we use in Ruby—and a little effort can go a really long way to build confidence in people who are just getting started.</strong></p>
<p>Rails is the 500lb gorilla in the Ruby world, so lets pick on it for a second. As of Rails 7.0, here’s what people see when they run <code>rails new</code> on their command line.</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-ixxlbltd"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-ixxlbltd"><span class="nv">$ </span>rails new hello-rails
Ignoring rbs-2.8.2 because its extensions are not built. Try: gem pristine rbs <span class="nt">--version</span> 2.8.2
create
create README.md
create Rakefile
<span class="c"># ... A bunch of stuff happens here ...</span>
Appending: pin_all_from <span class="s2">"app/javascript/controllers"</span>, under: <span class="s2">"controllers"</span>
append config/importmap.rb
</code></pre>
</div>
</div>
<p>Then, somehow, they have to figure out how to boot the development server via <code>bin/rails server</code> and see this screen.</p>
<p><img alt="Rails 7.0 introduction screen" src="/images/rails-7-0-intro.webp" /></p>
<p>The screen tells us what version of Rails we’re running and the Ruby version. That information might be helpful for people who are running multiple versions of Ruby and Rails on their machines, but it doesn’t really tell people what to do next.</p>
<p>If this our first time in Rails we might look at that screen and wonder “uh … ok? Now what?”. Each little pause and utterance slowly chips away at the confidence people who are just getting started with Rails. It’s not a good look if we want to attract more junior engineers to the ecosystem.</p>
<p>We can do better!</p>
<h2 id='tell-people-what-they-should-do-next' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#tell-people-what-they-should-do-next' aria-label='Anchor'></a>Tell People What They Should Do Next</h2>
<p>What if instead of letting people slam walls we give them little hints of what to do next? It’s really not that much extra effort to give people this experience.</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-70cl1u0r"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-70cl1u0r"><span class="nv">$ </span>rails new hello-rails
Ignoring rbs-2.8.2 because its extensions are not built. Try: gem pristine rbs <span class="nt">--version</span> 2.8.2
create
create README.md
create Rakefile
<span class="c"># ... A bunch of stuff happens here ...</span>
Appending: pin_all_from <span class="s2">"app/javascript/controllers"</span>, under: <span class="s2">"controllers"</span>
append config/importmap.rb
Rails project created and dependencies installed.
Now switch to the directory where the Rails app was created:
<span class="nv">$ </span><span class="nb">cd </span>hello-rails
Then boot the development server:
<span class="nv">$ </span>./bin/rails server
You can always get <span class="nb">help </span>by running:
<span class="nv">$ </span>./bin/rails <span class="nb">help</span>
</code></pre>
</div>
</div>
<p>Let’s do the first thing and boot the development server.</p>
<p><img alt="A Rails 7.0 introduction screen that tells the person to edit their code next" src="/images/rails-imagine-intro.webp" /></p>
<p>It doesn’t seem like much, but little morsels of what’s next prevent the gremlins of self-doubt and imposter syndrome from chipping away at a new persons’ confidence. “Oh cool! I can go there and start editing my project code”.</p>
<p>The best part? If you’re a seasoned Rails veteran, these little things don’t really get in your way. Win win!</p>
<h2 id='so-many-possibilities' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#so-many-possibilities' aria-label='Anchor'></a>So Many Possibilities</h2>
<p>There’s all sorts of possibilities to improve this flow to instill confidence in people who are new to the ecosystem.</p>
<ul>
<li>A link to the <a href='https://guides.rubyonrails.org' title=''>Rails Guides</a> and <a href='https://api.rubyonrails.org' title=''>Documentation</a>
</li><li>Tell the person to run <code>rails help</code> from their project folder or display the commands on this page.
</li><li>Give them a specific command to run like <code>rails generate scaffolding Posts title:string content:text</code> so they can experience a quick “ah ha!” moment.
</li><li>Link to the Rails Forums or Discord community for help.
</li></ul>
<p>The list goes on, and there’s some great examples of Frameworks out there doing this, like <a href='/phoenix-files' title=''>Phoenix</a>.</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-218wneff"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-218wneff"><span class="nv">$ </span>mix phx.new hello
<span class="k">*</span> creating hello/config/config.exs
<span class="c"># ... A bunch of stuff happens here ...</span>
<span class="k">*</span> running mix assets.setup
<span class="k">*</span> running mix deps.compile
We are almost there! The following steps are missing:
<span class="nv">$ </span><span class="nb">cd </span>hello
Then configure your database <span class="k">in </span>config/dev.exs and run:
<span class="nv">$ </span>mix ecto.create
Start your Phoenix app with:
<span class="nv">$ </span>mix phx.server
You can also run your app inside IEx <span class="o">(</span>Interactive Elixir<span class="o">)</span> as:
<span class="nv">$ </span>iex <span class="nt">-S</span> mix phx.server
</code></pre>
</div>
</div>
<p>Then after booting the server we see a page that links to guides & docs and the Elixir community.</p>
<p><img alt="Phoenix introduction screen" src="/images/pheonix-1-0-intro.webp" /></p>
<p>What a difference!</p>
<h2 id='dont-be-overwhelmingly-helpful' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#dont-be-overwhelmingly-helpful' aria-label='Anchor'></a>Don’t be Overwhelmingly Helpful</h2>
<p>It’s tempting to want to do All The Things<sup>TM</sup> and provide a huge list of links to users to get them started, but you’ll want to be careful that you don’t overwhelm people with too many options.</p>
<p>Think of the first day on the job or at a new school—there’s <em>lots</em> of stuff coming to you all at once. Under those circumstances, its best to consume information in little bite-size morsels.</p>
<h2 id='we-can-all-help' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#we-can-all-help' aria-label='Anchor'></a>We Can All Help!</h2>
<p>If you maintain open-source libraries, documentation, or any parts of the Rails and Ruby ecosystem, try running through whatever your <code>new</code> workflow is and make sure you’re always giving people a path forward that instills confidence and doesn’t overwhelm.</p>
<p>It might not seem like a big deal, but if enough people do it, the entire ecosystem will be just a tad more welcoming to people who are just getting started. I <a href='https://github.com/rails/rails/pull/47620' title=''>opened a pull request</a> on the Rails Github repository with the ideas from above—what are some things you can do to help make Ruby more welcoming to the next generation of developers?</p>
<p>Let’s do this! 👊</p>
/ruby-dispatch/little-gestures-of-confidence/assets/little-gestures-thumbnail.webpCI/CDhttps://fly.io/ruby-dispatch/ci-cd/2023-03-06T00:00:00+00:002023-11-21T21:08:38+00:00<p>You’ve been a model developer. You’ve placed your source code under version control and posted it to GitHub. You’ve got a suite of tests, and they run green. You’ve deployed your software to production.</p>
<p>That’s a lot of work. You deserve a break. Let’s automate these tasks. The goal is to only deploy changes that pass the tests that you have defined. The good news is that GitHub actions makes this easy.</p>
<p>To get started, place the following in <code>.github/workflows/ci-cd.yml</code>:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-68wyq2yn"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-68wyq2yn">name: CI_CD
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- run: rake test:all
deploy:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
</code></pre>
</div>
</div>
<p>In a few short lines, what the above says is:</p>
<div class="callout"><p>On every put and every pull request:</p>
<ul>
<li><p>check out your source, setup ruby, and run all your tests</p>
</li><li><p>if the tests pass, and the branch is <code>main</code>:
</p><ul style="margin-bottom: 0"><li>check out your source, setup flyctl, and deploy</li></ul><p></p>
</li></ul>
</div>
<p>Note: the above assumes that your main branch is named <code>main</code>. Some older GitHub repositories use <code>master</code>. Adjust the <code>github.ref</code> check as needed.</p>
<p>The bad news is that if you try this with pretty much the simplest Rails application, it will fail:</p>
<div class="highlight-wrapper group relative sh">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-3is30wsk"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-3is30wsk">rails new todolist
<span class="nb">cd </span>todolist
bin/rails generate scaffold Todo item
bin/rails db:migrate
</code></pre>
</div>
</div>
<p>There are two reasons for the failures. The first is an obscure bug in <a href='https://bugs.ruby-lang.org/issues/19158#note-10' title=''>Ruby</a>. At the moment, Rails 7.1’s fix is to <a href='https://github.com/rails/rails/pull/47515' title=''>remove the debug gem</a>. This will eventually work itself out.</p>
<p>For the moment, if you are on Ruby 3.2 or don’t actively use the debug gem, the workaround is to run the following command to do the same with your project:</p>
<div class="highlight-wrapper group relative sh">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-e9pd40v2"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-e9pd40v2">bundle remove debug
</code></pre>
</div>
</div>
<p>The second problem is that Rails system tests launch a browser, which by default need a display, and the GitHub servers don’t have one. So the solution is to change <code>:chrome</code> to <code>:headless_chrome</code> in one file:</p>
<div class="highlight-wrapper group relative diff">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-hnykst54"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-hnykst54"> require "test_helper"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
<span class="gd">- driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
</span><span class="gi">+ driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
</span> end
</code></pre>
</div>
</div>
<p>The next step is to provide your flyctl access token to Github as a secret.</p>
<p>Start by running the following command:</p>
<div class="highlight-wrapper group relative cmd">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-nge5tqee"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight cmd'><code id="code-nge5tqee">flyctl auth token
</code></pre>
</div>
</div>
<p>Now you have a token you need to make it available to GitHub Actions that run against your repository. For that, there’s secrets in the repository’s settings. GitHub provides four combinations: Environment and Repository, and Secrets and Variables. Click on the green “New repository secret” button in the top left, pop our secret under the <code>FLY_API_TOKEN</code> name, and we are ready.</p>
<p>All that is left is for you to commit your changes and…</p>
<div class="highlight-wrapper group relative cmd">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-8pm1r48t"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight cmd'><code id="code-8pm1r48t">git push
</code></pre>
</div>
</div><figure class="post-cta">
<figcaption>
<h1>You can play with this right now.</h1>
<p>It’ll take less than 10 minutes to get your Rails application running globally.</p>
<a class="button bg-white text-violet-500 hover:bg-opacity-75 hover:text-violet-700 transition-colors" href="https://fly.io/docs/rails/">
Try Fly for free <span class='opacity-50'>→</span>
</a>
</figcaption>
<div class="image-container">
<img src="/static/images/cta-kitty.webp" srcset="/static/images/cta-kitty@2x.webp 2x" alt="">
</div>
</figure>
<p>This is just a taste of what GitHub actions can do for your application.
The one thing we haven’t covered is testing your Dockerfile itself.
While this can be done using <a href='https://github.com/docker/setup-buildx-action#readme' title=''><code>docker/setup-buildx-action@v2</code></a>, <a href='https://fly.io/ruby-dispatch/health-checks/' title=''>health checks</a> already do this and more.</p>
/ruby-dispatch/ci-cd/assets/ci-cd-thumbnail.webpHealth Checkshttps://fly.io/ruby-dispatch/health-checks/2023-02-21T00:00:00+00:002023-11-21T21:08:38+00:00<p><strong class='font-[550] text-navy-950'>Rails 7.1 is adding <a href='https://github.com/rails/rails/pull/47217#discussion_r1098112364' title=''>discoverable</a> <a href='https://github.com/rails/rails/pull/46936' title=''>health checks</a>, which <code>fly launch</code> will automatically configure and monitor. This page will tell you what you need to know.</strong></p>
<p>No matter how well you plan, you will always need to be prepared to deal with
unforeseen and unforeseeable events. As Fly.io will route requests to the
nearest <em>healthy</em> server, deploying your application across multiple regions
not only means that you will be serving requests close to your users when
things are running smoothly, it also means that you will be able to
continue processing requests when there are isolated or even regional outages.</p>
<p>Let’s start with a demo. Run the following commands in a terminal window.</p>
<figure class="post-cta">
<figcaption>
<h1>You can play with this right now.</h1>
<p>It’ll take less than 10 minutes to get your Rails application running globally.</p>
<a class="button bg-white text-violet-500 hover:bg-opacity-75 hover:text-violet-700 transition-colors" href="https://fly.io/docs/rails/">
Try Fly for free <span class='opacity-50'>→</span>
</a>
</figcaption>
<div class="image-container">
<img src="/static/images/cta-rabbit.webp" srcset="/static/images/cta-rabbit@2x.webp 2x" alt="">
</div>
</figure>
<div class="highlight-wrapper group relative shell">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-eigpa4ue"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-eigpa4ue">rails new health_demo <span class="nt">--main</span> <span class="nt">--minimal</span>
<span class="nb">cd </span>health_demo
<span class="nb">echo</span> <span class="s1">'Rails.application.routes.draw {root "rails/welcome#index"}'</span> <span class="se">\</span>
<span class="o">>></span> config/routes.rb
fly launch
</code></pre>
</div>
</div>
<p>At this point you will be asked a series of questions. Feel free to accept the
default by pressing enter in response to each question. You are now ready to deploy:</p>
<div class="highlight-wrapper group relative shell">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-tdnydorq"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-tdnydorq">fly deploy
</code></pre>
</div>
</div>
<p>This command won’t complete until the health checks pass and your application
is ready to process requests. You can visit your application by using the
<code>fly open</code> command.</p>
<p>Now let’s take a look at the results of the latest health checks:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-ek7nqed8"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-ek7nqed8">fly checks list
</code></pre>
</div>
</div>
<p>You will see two health checks were run. One is a TCP check which verifies
that your application is listening for requests. The other is a HTTP check
which verifies that your application is capable of producing responses.</p>
<p>An example of where your application might be listening for responses but
not actually ready to process a request is when you have left the puma
defaults at 5 threads and you currently are experiencing a spike in traffic.
In cases like these you might want to review your <code>config/puma.rb</code>, or
deploy more servers.</p>
<p>Another example is where you have a CDN or an nginx server handing static
requests that pass or proxy the remainder to your Rails application, and
your CDN or nginx server is healthy and your Rails server is, um, not.</p>
<p>If you like, you can add a nginx server to your deployment by running
the following commands:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-p3qcle9i"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-p3qcle9i">bin/rails generate dockerfile --nginx --force
fly deploy
</code></pre>
</div>
</div>
<p>An example of a successful Rails 7.1 response:</p>
<div class="highlight-wrapper group relative html">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-4xtmo7c8"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-4xtmo7c8">200 OK Output: <span class="nt"><html><body</span> <span class="na">style=</span><span class="s">"background-color: green"</span><span class="nt">></body></html></span>[✓]
</code></pre>
</div>
</div>
<p>So far we’ve only deployed our application to one region. Now lets make it
interesting and scale up to two regions. I’m in the US, so I’ll deploy my
second server in Europe:</p>
<div class="highlight-wrapper group relative shell">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-1x2wjc6i"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-1x2wjc6i">fly regions add cdg
fly scale count 2
</code></pre>
</div>
</div>
<p>Feel free to pick <a href='https://fly.io/docs/reference/regions/#fly-io-regions' title=''>another region</a>. For demo purposes, you might want to avoid the Paid Plan Only regions.</p>
<p>If you rerun the <code>fly checks list</code> command you will see four checks now. If
you run this command quickly enough you might catch it before the second server
has fully started.</p>
<p>Also, try adding <code>--json</code> to the <code>fly checks list</code> command. This output could
be processed by scripts.</p>
<p>Before you try this on your own project: please check the <a href='https://fly.io/docs/rails/getting-started/dockerfiles/#scaling' title=''>Rails
FAQ</a> for
important information on how to get Fly.io to prepare your databases once per
deploy and not once per server.</p>
<h2 id='behind-the-scenes' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#behind-the-scenes' aria-label='Anchor'></a>Behind the scenes</h2>
<p>So far everything has been taken care of for you. But perhaps your application
isn’t yet on Rails 7.1 (which is understandable as it hasn’t been released).
Or perhaps you want to tweak how often health checks are run. Either way, we
have you covered.</p>
<p>All you need to do to get a health check is to add a route, and tell us to call
it.</p>
<p>The most simplest route will do. For example, you can add the following to
<code>config/routes.rb</code>:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-wy907euv"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-wy907euv"><span class="n">get</span> <span class="s2">"/up"</span><span class="p">,</span> <span class="ss">to: </span><span class="nb">proc</span> <span class="p">{</span> <span class="p">[</span><span class="mi">200</span><span class="p">,</span> <span class="p">{},</span> <span class="p">[</span><span class="s2">"ok"</span><span class="p">]]</span> <span class="p">},</span>
<span class="ss">as: :rails_health_check</span>
</code></pre>
</div>
</div>
<p>This won’t be as pretty as the Rails 7.1 with its fancy green background, but
in every way that is important it will get the job done.</p>
<p>If you don’t like <code>/up</code> as the endpoint name, feel free to change it. This
goes for the one that Rails 7.1 provides too.</p>
<p>If this route is present before you run <code>fly launch</code>, your application will
be automatically configured to call this endpoint.</p>
<p>If you have already run <code>fly launch</code>, it is not too late to tell us to
call your health check. All you need to do is add the following to your
<code>fly.toml</code> file:</p>
<div class="highlight-wrapper group relative toml">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-xxjazodk"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-xxjazodk"> <span class="nn">[[services.http_checks]]</span>
<span class="py">interval</span> <span class="p">=</span> <span class="mi">10000</span>
<span class="py">grace_period</span> <span class="p">=</span> <span class="s">"5s"</span>
<span class="py">method</span> <span class="p">=</span> <span class="s">"get"</span>
<span class="py">path</span> <span class="p">=</span> <span class="s">"/up"</span>
<span class="py">protocol</span> <span class="p">=</span> <span class="s">"http"</span>
<span class="py">restart_limit</span> <span class="p">=</span> <span class="mi">0</span>
<span class="py">timeout</span> <span class="p">=</span> <span class="mi">2000</span>
<span class="py">tls_skip_verify</span> <span class="p">=</span> <span class="kc">false</span>
<span class="nn">[services.http_checks.headers]</span>
</code></pre>
</div>
</div>
<p>More information on what each value means can be found in our
<a href='https://fly.io/docs/reference/configuration/' title=''>reference documentation</a>.
Adjust to your tastes, and then run <code>fly deploy</code>.</p>
<h2 id='going-beyond-quot-up-quot' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#going-beyond-quot-up-quot' aria-label='Anchor'></a>Going beyond “/up”</h2>
<p>Your Rails application undoubtedly does more than display a splash screen.
The more moving parts you have, the more that can go wrong. You can
run out of memory, or run out of disk space. You might be using a database
running on another machine or hosted by a third party. You may be using
redis. Or perhaps Amazon, Azure, or Google Cloud Services.</p>
<p>Any one of these could go down at any time. Often such outages are local or
regional. If you monitor your dependencies, Fly.io will route requests around
the failures until the outage is cleared.</p>
<p>There is nothing magical about the implementation of the “/up” route.
It can be routed to any controller action in your application. All
that application needs to do is produce a successful response when
things are good. And return a response like the following when
things are not-so-good:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-7hkmip8b"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-7hkmip8b"><span class="n">render</span> <span class="ss">plain: </span><span class="s2">"BUSY"</span><span class="p">,</span> <span class="ss">status: :service_unavailable</span>
</code></pre>
</div>
</div>
<p>Don’t worry too much about the text of the response or the status code used.
<code>:service_unavailable</code> is good for outages that you are prepared and explicitly
check for. But if your application gets to the point where it is incapable of
producing a coherent response, <code>500 :internal_server_error</code> works too.</p>
<p>Best of all, you don’t have to start from scratch writing these
controllers. <a href='https://github.com/basecamp/easymon' title=''>easymon</a> and
<a href='https://github.com/linqueta/rails-healthcheck' title=''>rails-healhcheck</a> both
will not only provide examples which check for common dependencies,
they also provide means for you to add your own checks.</p>
<p>In many cases, adding a check is becomes a matter of adding a one liner
to to a configuration file and deploying.</p>
<p>Neither of these gems produce routes that are currently autodiscoverable, but
we have reached out to both
(<a href='https://github.com/basecamp/easymon/issues/34' title=''>easymon</a>,
<a href='https://github.com/linqueta/rails-healthcheck/pull/77' title=''>rails-healthcheck</a>).
In the meanwhile, you can scroll back on this page to see what you need to add
to your <code>fly.toml</code> to tell fly.io to check these endpoints.</p>
<h2 id='recap' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#recap' aria-label='Anchor'></a>Recap</h2>
<p>At this point, you’ve:</p>
<ul>
<li>run a demo where an application has been deployed to two regions,
and health checks are provisioned for you automatically.
</li><li>seen how you can add health checks to an existing application and
tailor parameters like how often the checks are to be called.
</li><li>found two gems that make it easy to extend these checks to handle third
party failures.
</li></ul>
<p>With this knowledge you are prepared to run your application in as many
regions as it takes to make you comfortable that your application can
survive an outage.</p>
/ruby-dispatch/health-checks/assets/health-check-thumbnail.webpRails on Dockerhttps://fly.io/ruby-dispatch/rails-on-docker/2023-01-19T00:00:00+00:002023-11-21T21:08:38+00:00<p><strong class='font-[550] text-navy-950'>Rails 7.1 is getting an</strong> <a href='https://github.com/rails/rails/pull/46794' title=''><strong class='font-[550] text-navy-950'>official Dockerfile</strong></a><strong class='font-[550] text-navy-950'>, which should make it easier to deploy Rails applications to production environments that support Docker. Think of it as a pre-configured Linux box that will work for most Rails applications.</strong></p>
<p>That means you’ll start seeing a <code>Dockerfile</code> in the project directory of a lot more Rails apps. If you’re not familiar with Docker, you might open the file and see a few things that look familiar like some bash commands, but some other things might be new and foreign to you.</p>
<p>Let’s dive into what’s in a <code>Dockerfile</code> so its less of a mystery, but first let’s have a look at how Fly.io uses Docker so you better understand how Docker fits into a Rails stack.</p>
<h2 id='how-does-fly-io-use-docker' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#how-does-fly-io-use-docker' aria-label='Anchor'></a>How does Fly.io use Docker?</h2>
<p>Fun fact! Fly.io doesn’t actually run Docker in production—rather it uses a Dockerfile to <a href='https://fly.io/blog/docker-without-docker/' title=''>create a Docker image, also known as an OCI image, that it runs as a Firecracker VM</a>. What does that mean for you? Not much really. For all practical purposes you’ll describe your applications’ production machine in a Dockerfile and Fly.io transparently handles the rest.</p>
<p>The great thing about Dockerfiles is it makes standardizing production deployments possible, which for most developers means its easier to deploy applications to hosts that support Docker, like <a href='/docs/rails' title=''>Fly.io</a>.</p>
<p>What’s a Dockerfile? It’s a text file with a bunch of declarations and Linux commands that describe what needs to be installed and executed to get an application running. This file is given to a bunch of fancy software that configures a Linux distribution to the point where it can run your application.</p>
<p>You can think of each command in the file as a “layer”. At the bottom of the layer is a Linux distribution, like Ubuntu. Each command adds another layer to the configuration until eventually all the packages, configurations, and application code are in the container and your app can run. This layering is important for caching commands, which make deployments fast if done correctly.</p>
<p>Let’s get into it.</p>
<h2 id='a-closer-look-at-the-rails-dockerfile' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#a-closer-look-at-the-rails-dockerfile' aria-label='Anchor'></a>A closer look at the Rails Dockerfile</h2>
<p>At the time of this writing, the default Rails 7.1 Dockerfile looks like:</p>
<div class="highlight-wrapper group relative dockerfile">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-7sxva1z3"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-7sxva1z3"><span class="c"># Make sure it matches the Ruby version in .ruby-version and Gemfile</span>
<span class="k">ARG</span><span class="s"> RUBY_VERSION=3.2.0</span>
<span class="k">FROM</span><span class="s"> ruby:$RUBY_VERSION</span>
<span class="c"># Install libvips for Active Storage preview support</span>
<span class="k">RUN </span>apt-get update <span class="nt">-qq</span> <span class="o">&&</span> <span class="se">\
</span> apt-get <span class="nb">install</span> <span class="nt">-y</span> build-essential libvips <span class="o">&&</span> <span class="se">\
</span> apt-get clean <span class="o">&&</span> <span class="se">\
</span> <span class="nb">rm</span> <span class="nt">-rf</span> /var/lib/apt/lists/<span class="k">*</span> /usr/share/doc /usr/share/man
<span class="c"># Rails app lives here</span>
<span class="k">WORKDIR</span><span class="s"> /rails</span>
<span class="c"># Set production environment</span>
<span class="k">ENV</span><span class="s"> RAILS_LOG_TO_STDOUT="1" \</span>
RAILS_SERVE_STATIC_FILES="true" \
RAILS_ENV="production" \
BUNDLE_WITHOUT="development"
# Install application gems
<span class="k">COPY</span><span class="s"> Gemfile Gemfile.lock ./</span>
<span class="k">RUN </span>bundle <span class="nb">install</span>
<span class="c"># Copy application code</span>
<span class="k">COPY</span><span class="s"> . .</span>
<span class="c"># Precompile bootsnap code for faster boot times</span>
<span class="k">RUN </span>bundle <span class="nb">exec </span>bootsnap precompile <span class="nt">--gemfile</span> app/ lib/
<span class="c"># Precompiling assets for production without requiring secret RAILS_MASTER_KEY</span>
<span class="k">RUN </span><span class="nv">SECRET_KEY_BASE_DUMMY</span><span class="o">=</span>1 bundle <span class="nb">exec </span>rails assets:precompile
<span class="c"># Entrypoint prepares the database.</span>
<span class="k">ENTRYPOINT</span><span class="s"> ["/rails/bin/docker-entrypoint"]</span>
<span class="c"># Start the server by default, this can be overwritten at runtime</span>
<span class="k">EXPOSE</span><span class="s"> 3000</span>
<span class="k">CMD</span><span class="s"> ["./bin/rails", "server"]</span>
</code></pre>
</div>
</div>
<p>At the top of the file, we set the Ruby version.</p>
<div class="highlight-wrapper group relative dockerfile">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-9p6haod2"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-9p6haod2"><span class="c"># Make sure it matches the Ruby version in .ruby-version and Gemfile</span>
<span class="k">ARG</span><span class="s"> RUBY_VERSION=3.2.0</span>
<span class="k">FROM</span><span class="s"> ruby:$RUBY_VERSION</span>
</code></pre>
</div>
</div>
<p>The version gets plugged into the <code>FROM</code> command, which ends up looking like <code>FROM ruby:3.2.0</code>. Where is <code>ruby:3.2.0</code>? It’s a <a href='https://hub.docker.com/_/ruby/' title=''>Docker image</a> that some community members have graciously configured for us that gets us a Linux distribution running Ruby 3.2. That’s not enough to run a Rails application; we need to add a few more layers to the image.</p>
<p>Next up the Dockerfile installs Linux packages needed to run certain Rails gems in Linux. <code>libvibs</code> is a native library used to resize images for ActiveSupport. Other packages could be added here, like a Postgres, MAQL, or SQLite client. Other gems may depend on Linux packages too. For example, a popular XML parsing library, Nokogiri, depends on libxml. Those are not included in this list because the <code>ruby</code> image already includes them.</p>
<div class="highlight-wrapper group relative dockerfile">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-qwxdrriy"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-qwxdrriy"><span class="c"># Install libvips for Active Storage preview support</span>
<span class="k">RUN </span>apt-get update <span class="nt">-qq</span> <span class="o">&&</span> <span class="se">\
</span> apt-get <span class="nb">install</span> <span class="nt">-y</span> build-essential libvips <span class="o">&&</span> <span class="se">\
</span> apt-get clean <span class="o">&&</span> <span class="se">\
</span> <span class="nb">rm</span> <span class="nt">-rf</span> /var/lib/apt/lists/<span class="k">*</span> /usr/share/doc /usr/share/man
</code></pre>
</div>
</div>
<p><code>apt-get</code> is a Linux package manager that always looks strange in a Dockerfile because of all the command it does before and after installing a package. Let’s break it down line-by-line.</p>
<p>First, <code>apt-get update -qq</code> tells Linux to download a manifest of all the packages that are available for download from <code>apt-get</code>.</p>
<p>The second line is the one you care about and might need to change. <code>apt-get install -y build-essential libvips</code> installs two packages and the <code>-y</code> automatically answers “yes” when it asks if you’re sure you want to install the packages.</p>
<p>Everything after that removes the manifest files and any temporary files downloaded during this command. It’s necessary to remove all these files in this command to keep the size of the Docker image to a minimum. Smaller Dockerfiles mean faster deployments.</p>
<p>Next the working directory is set.</p>
<div class="highlight-wrapper group relative dockerfile">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-36loieke"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-36loieke"><span class="c"># Rails app lives here</span>
<span class="k">WORKDIR</span><span class="s"> /rails</span>
</code></pre>
</div>
</div>
<p>This creates the <code>./rails</code> folder inside the docker image. All of the lines in the Dockerfile after this are run from that directory and any files added to the image are put in that directory. It’s the equivalent of <code>mkdir -p ./rails && cd ./rails</code>.</p>
<p>Next a few environment variables are set.</p>
<div class="highlight-wrapper group relative dockerfile">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-b04whpfw"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-b04whpfw"><span class="c"># Set production environment</span>
<span class="k">ENV</span><span class="s"> RAILS_LOG_TO_STDOUT="1" \</span>
RAILS_SERVE_STATIC_FILES="true" \
RAILS_ENV="production" \
BUNDLE_WITHOUT="development"
</code></pre>
</div>
</div>
<p>What are these you ask?</p>
<ul>
<li><code>RAILS_LOG_TO_STDOUT</code> - Rails log output is sent to STDOUT instead of a file. STDOUT, or standard out, makes it possible for <code>docker logs</code> to view the output of whatever is running on the container. <a href='https://12factor.net/logs' title=''>The Twelve-Factor App has a good explanation</a> of why logs should be written to STDOUT.
</li><li><code>RAILS_SERVE_STATIC_FILES</code> - This instructs Rails to <em>not</em> serve static files. It’s always been recommended to have a server like nginx serve up images, CSS, JavaScripts, and other static files by a server that’s not Ruby for better performance.
</li><li><code>RAILS_ENV</code> - Instructs Rails to boot with <code>production</code> gems and with the configuration from <code>config/environments/production.rb</code>.
</li><li><code>BUNDLE_WITHOUT</code> - If you’ve ever looked in your application’s Gemfile, you’ll notice there’s gems tagged with the <code>development</code> group like <code>web-console</code>. These gems are not needed or wanted in a production environment because they would either slow things down, not be used, or pose a security risk. This command tells bundler to leave out all the <code>development</code> gems.
</li></ul>
<p>Time to install the gems! First Docker copies the <code>Gemfile</code> and <code>Gemfile.lock</code> from our workstation or CI’s server project directory into the containers <code>./rails</code> directory (remember the thing that was set by <code>WORKDIR</code> above? This is it!)</p>
<div class="highlight-wrapper group relative dockerfile">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-tfkofpgd"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-tfkofpgd"><span class="c"># Install application gems</span>
<span class="k">COPY</span><span class="s"> Gemfile Gemfile.lock ./</span>
<span class="k">RUN </span>bundle <span class="nb">install</span>
</code></pre>
</div>
</div>
<p><code>bundle install</code> is run against the Gemfiles that were copied over. This installs the gems inside the container, which we’ll need for our Rails application to run.</p>
<p>Something you might be asking yourself, “why not copy the entire application from my workstation and then run <code>bundle install</code>?”. Great question! Each “ALLCAPS” directive in a Dockerfile, like <code>RUN</code>, <code>COPY</code>, etc. are “layers” that get cached. If you didn’t handle copying the Gemfile and running bundler as a separate layer, you’d have to run <code>bundle install</code> every time you deployed Rails, even if you didn’t change the gem. That would take forever!</p>
<p>Making it a separate layer means you only have to update the bundle when the <code>Gemfile</code> changes. In other words, if you only change application code, you can skip running bundle and jump right into the next layer, which saves loads of time between deploys.</p>
<p>Finally the Rails application code files are copied from your computer or CI machine to the <code>WORKDIR</code> set above, which is <code>./rails</code> in the image.</p>
<div class="highlight-wrapper group relative dockerfile">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-9dalt6g5"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-9dalt6g5"><span class="c"># Copy application code</span>
<span class="k">COPY</span><span class="s"> . .</span>
</code></pre>
</div>
</div>
<p>When we boot the Docker image and the Rails server, we want it to come online as quickly as possible so our deploys are faster, so the image copies over the bootsnap cache to make that happen.</p>
<div class="highlight-wrapper group relative dockerfile">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-vh5fe5sg"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-vh5fe5sg"><span class="c"># Precompile bootsnap code for faster boot times</span>
<span class="k">RUN </span>bundle <span class="nb">exec </span>bootsnap precompile <span class="nt">--gemfile</span> app/ lib/
</code></pre>
</div>
</div>
<p>At this point all the files needed to run the server are copied over from the workstation to the Docker image, with the exception of files listed in <code>.dockerignore</code>which typically include the <code>.git</code> directory, log files, etc.</p>
<p>Now its time to compile JavaScript, stylesheet, and image assets!</p>
<div class="highlight-wrapper group relative dockerfile">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-67hx45bo"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-67hx45bo"><span class="c"># Precompiling assets for production without requiring secret RAILS_MASTER_KEY</span>
<span class="k">RUN </span><span class="nv">SECRET_KEY_BASE_DUMMY</span><span class="o">=</span>1 bundle <span class="nb">exec </span>rails assets:precompile
</code></pre>
</div>
</div>
<p>This is a bit if a hack. Rails <em>requires</em> a secret key to keep sessions and other cryptographic Rails features secure, but for an asset compilation, including the actual secret key is not needed and is therefore a liability. Instead <code>SECRET_KEY_BASE_DUMMY=1</code> is passed into the compilation task to tell Rails, “ignore requiring a secret key”.</p>
<p>The most important part of this command is <code>bundle exec rails assets:precompile</code>, which runs whatever compilation steps are needed to minify and fingerprint assets so they load quickly in production.</p>
<p>The <code>ENTRYPOINT</code> directive in Docker acts like a wrapper.</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-mggi7lvu"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-mggi7lvu"># Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
</code></pre>
</div>
</div>
<p>The best thing to do is look at the contents of the <code>ENTRYPOINT</code> script, which lives at <code>./bin/docker-entrypoint</code></p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-tgmb7mrf"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-tgmb7mrf"><span class="c">#!/bin/bash</span>
<span class="c"># If running the rails server then create or migrate existing database</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="k">${</span><span class="p">*</span><span class="k">}</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"./bin/rails server"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
./bin/rails db:prepare
<span class="k">fi
</span><span class="nb">exec</span> <span class="s2">"</span><span class="k">${</span><span class="p">@</span><span class="k">}</span><span class="s2">"</span>
</code></pre>
</div>
</div>
<p>The script checks to see if the <code>CMD</code>, below, is running <code>./bin/rails server</code>. If its running the server, it will make sure it runs a database migration before it boots the application. If you don’t want Rails to automatically run migrations when you deploy, you could comment out or remove the <code>ENTRYPOINT</code> directive in the <code>Dockerfile</code>.</p>
<p>The last thing in all Dockerfiles is how to boot the application server.</p>
<div class="highlight-wrapper group relative dockerfile">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-bjehtgb9"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-bjehtgb9"><span class="c"># Start the server by default, this can be overwritten at runtime</span>
<span class="k">EXPOSE</span><span class="s"> 3000</span>
<span class="k">CMD</span><span class="s"> ["./bin/rails", "server"]</span>
</code></pre>
</div>
</div>
<p><code>EXPOSE 3000</code> tells Docker the application will listen on port 3000, which is the default for <code>bin/rails server</code>, as you see in your Rails development environment.</p>
<h2 id='take-it-for-a-spin' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#take-it-for-a-spin' aria-label='Anchor'></a>Take it for a spin</h2>
<p>You can expect a lot of changes between now and when Rails 7.1 is released. For example, we’re currently exploring extracting Dockerfile generation out of <code>railties</code> and moving it into its own gem at <a href='https://rubygems.org/gems/dockerfile-rails' title=''>https://rubygems.org/gems/dockerfile-rails</a>. That means you can use this today with your current Rails project. If you want to try it out, first install the gem to your Rails app.</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-yj1lf0aj"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-yj1lf0aj"><span class="nv">$ </span>bundle add dockerfile-rails
</code></pre>
</div>
</div>
<p>Then generate the <code>Dockerfile</code> with the <code>dockerfile</code> command.</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-aypr3m37"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-aypr3m37"><span class="nv">$ </span>./bin/rails dockerfile
</code></pre>
</div>
</div>
<p>Then checkout the <code>Dockerfile</code> that’s now at the root of your project. You can then deploy it by <a href='https://fly.io/docs/flyctl/installing/' title=''>installing Fly.io</a>, running the following command, and following the instructions.</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-m86avc4t"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-m86avc4t"><span class="nv">$ </span>fly launch
</code></pre>
</div>
</div>
<p>Fly.io will ask you a few questions and within a few minutes, you should see your Rails app running in production.</p>
<h2 id='theres-a-lot-of-different-ways-to-configure-a-dockerfile' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#theres-a-lot-of-different-ways-to-configure-a-dockerfile' aria-label='Anchor'></a>There’s a lot of different ways to configure a Dockerfile</h2>
<p>The official Rails Dockerfile will be a great starting place for most people, but as applications grow in complexity and the need to install additional packages arises, it might not be enough.</p>
<p>Fly.io has started putting together a collection of Dockerfile recipes in the <a href='https://fly.io/docs/rails/cookbooks/' title=''>Fly Rails Cookbooks</a>. You’ll find example Dockerfiles for all sorts of different Rails deployments including those that needs Node 19+ installed or for Rails API deployments.</p>
/ruby-dispatch/rails-on-docker/assets/rails-on-docker-thumbnail.webpMonkey Patch Responsiblyhttps://fly.io/ruby-dispatch/monkey-patch-responsibly/2022-12-27T00:00:00+00:002023-11-21T21:08:38+00:00<p><strong class='font-[550] text-navy-950'>What are the hazards of Monkey Patching in Ruby? How you can create a Monkey Patch that you can share <em>responsibly</em> and <em>safely</em> with the Ruby community without causing bugs from forgetting to remove the patch.</strong></p>
<p>We live in an imperfect world, which means sometimes you need to “get stuff out that solves the more immediate pain” to run cover while a more permanent fix gets put into place.</p>
<p>In Ruby, for better or for worse, we have a concept called “Monkey Patching”. It let’s you do stuff like this:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-8rikqt2j"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-8rikqt2j"><span class="c1"># Version 1.0 of Hello World</span>
<span class="k">class</span> <span class="nc">Hello</span>
<span class="k">def</span> <span class="nf">world</span>
<span class="nb">puts</span> <span class="s2">"Go away!"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># The patch</span>
<span class="k">module</span> <span class="nn">HelloPatch</span>
<span class="k">def</span> <span class="nf">world</span>
<span class="nb">puts</span> <span class="s2">"Hello world!"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">Hello</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">world</span> <span class="c1"># => "Go away!"</span>
<span class="c1"># Apply the patch</span>
<span class="no">Hello</span><span class="p">.</span><span class="nf">prepend</span> <span class="no">HelloPatch</span>
<span class="no">Hello</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">world</span> <span class="c1"># => "Hello world!"</span>
</code></pre>
</div>
</div>
<p>This makes it really easy to patch broken Ruby code—in this case we replaced the buggy “Go away!” greeting with the happier “Hello world!”</p>
<h2 id='the-problem' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-problem' aria-label='Anchor'></a>The problem</h2>
<p>The problem is when the upstream software is patched and a new version goes out—often times the monkey patch can stay in place and cause unexpected bugs.</p>
<p>Imagine if version 2.0 of our Hello World library fixed the grumpy bug.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-49ypqcxh"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-49ypqcxh"><span class="c1"># Version 2.0 of Hello World: now with more friendliness!</span>
<span class="k">class</span> <span class="nc">Hello</span>
<span class="k">def</span> <span class="nf">world</span>
<span class="nb">puts</span> <span class="s2">"Howdy super friend!"</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>When we update to the new software a few months later and forgot about our patch, we’d be surprised to see <code>"Hello world!"</code> instead of <code>"Howdy super friend!"</code>.</p>
<p>How can we monkey patch responsibly?</p>
<h2 id='only-apply-the-patch-to-specific-versions-of-a-library' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#only-apply-the-patch-to-specific-versions-of-a-library' aria-label='Anchor'></a>Only apply the patch to specific versions of a library</h2>
<p>The problem in the example above is one of version. We need a way to target our patches to specific versions of the “broken” software.</p>
<p>To look at a real world example, at Fly we have a problem where Redis servers have a 5 minute time-out. When the Redis connection times out, ActionCable doesn’t reconnect. Our first iteration of the fix? A monkey patch!</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-gesmogk7"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-gesmogk7"><span class="c1"># config/initializers/action_cable.rb</span>
<span class="nb">require</span> <span class="s1">'action_cable/subscription_adapter/redis'</span>
<span class="k">module</span> <span class="nn">ActionCableRedisListenerPatch</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">ensure_listener_running</span>
<span class="vi">@thread</span> <span class="o">||=</span> <span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">current</span><span class="p">.</span><span class="nf">abort_on_exception</span> <span class="o">=</span> <span class="kp">true</span>
<span class="n">conn</span> <span class="o">=</span> <span class="vi">@adapter</span><span class="p">.</span><span class="nf">redis_connection_for_subscriptions</span>
<span class="n">listen</span> <span class="n">conn</span>
<span class="k">rescue</span> <span class="o">::</span><span class="no">Redis</span><span class="o">::</span><span class="no">BaseConnectionError</span>
<span class="vi">@thread</span> <span class="o">=</span> <span class="vi">@raw_client</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="o">::</span><span class="no">ActionCable</span><span class="p">.</span><span class="nf">server</span><span class="p">.</span><span class="nf">restart</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">ActionCable</span><span class="o">::</span><span class="no">SubscriptionAdapter</span><span class="o">::</span><span class="no">Redis</span><span class="o">::</span><span class="no">Listener</span><span class="p">.</span><span class="nf">prepend</span><span class="p">(</span><span class="no">ActionCableRedisListenerPatch</span><span class="p">)</span>
</code></pre>
</div>
</div>
<p>The problem with putting monkey patches in the <code>./config/initializers/*.rb</code> directory is the same as before: a few months later we forget it’s there and when the newer version fixes it, the monkey patch could cause bugs that are hard to track down.</p>
<p>A better to handle project monkey patches like this is by putting something like this at the top of each patch:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-tqmklwi6"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-tqmklwi6"><span class="k">if</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">version</span> <span class="o">></span> <span class="no">Gem</span><span class="o">::</span><span class="no">Version</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"7.0.4"</span><span class="p">)</span>
<span class="n">error</span> <span class="s2">"Check if https://github.com/rails/rails/pull/45478 is fixed"</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>When we upgrade Rails, this will blow up your CI or dev environment so a person on your team can check the PR and understand if the patch needs to be applied.</p>
<h2 id='releasing-a-community-patch' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#releasing-a-community-patch' aria-label='Anchor'></a>Releasing a community patch</h2>
<p>How can we scale the approach above to work for an entire community of developers?</p>
<p>Fortunately we can use <code>gemspec</code>‘s to manage this in a responsible way.</p>
<p>Since I know this problem currently effects <code>actioncable</code> starting at <code>7.0.0</code>, and the current version of Action Cable, which is at <code>7.0.4</code>, I can specify that in my gemspec:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-qo6avnwv"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-qo6avnwv"><span class="n">spec</span><span class="p">.</span><span class="nf">add_dependency</span> <span class="s2">"actioncable"</span><span class="p">,</span> <span class="s2">">= 7.0"</span><span class="p">,</span> <span class="s2">"<= 7.0.4"</span>
</code></pre>
</div>
</div>
<p>When <code>actionable 7.0.5</code> is released and the user runs <code>bundle update</code>, nothing will happened because this dependency will keep <code>actioncable</code> pegged at <code>7.0.4</code>.</p>
<p>That’s a good thing! Unless of course the developer <em>wants</em> the newer version of Rails. Since they forgot about the patch, they open up their <code>Gemfile</code> and set to the latest version of Rails.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-4a6nc150"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-4a6nc150"><span class="n">gem</span> <span class="s2">"rails"</span><span class="p">,</span> <span class="s2">"7.0.5"</span>
</code></pre>
</div>
</div>
<p>When they run <code>bundle update</code>, they get an error:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-qxp3hyas"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-qxp3hyas">Could not update to Rails 7.0.5 because the gem actioncable_redis-reconnectand depends on Rails 7.0 to 7.0.4
</code></pre>
</div>
</div>
<p>“WTF is that <code>actioncable_redis-reconnect</code> gem!?” says the developer. So they go to <a href='https://gem.wtf/actioncable_redis-reconnect' title=''>https://gem.wtf/actioncable_redis-reconnect</a> in their browser and get all the relevant context they need about that patch.</p>
<p>From this point they could do the following to resolve the issue.</p>
<ol>
<li><p>### Open a PR to bump the actioncable dependency</p>
<p>Open a PR on the gem that bumps the actioncable dependency if the issue is still present in <code>actioncable</code>:</p>
<div class="highlight-wrapper group relative diff">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-sx4x0cy8"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-sx4x0cy8"><span class="gd">- spec.add_dependency "actioncable", ">= 7.0", "<= 7.0.4"
</span><span class="gi">+ spec.add_dependency "actioncable", ">= 7.0.4", "<= 7.0.5"
</span></code></pre>
</div>
</div>
<p>Bumping the version should only be done in the patch gem <em>after</em> the maintain has done the research to determine whether or not the monkey patch is still needed or is compatible with that release.</p>
</li><li><p>### Remove the monkey patch gem</p>
<p>Maybe the developer doesn’t care anymore, so they remove the monkey patch gem and they can upgrade to the latest version of Rails.</p>
</li><li><p>### Do nothing</p>
<p>You don’t always have to run the latest version of a framework, unless of course there’s a security patch that needs to be installed. In that case go back to 1.</p>
</li></ol>
<p>The important thing is that the monkey patch was not allowed to persist quietly causing subtle bugs in production for years.</p>
<h2 id='deprecating-the-community-patch' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#deprecating-the-community-patch' aria-label='Anchor'></a>Deprecating the community patch</h2>
<p>When the issue is fixed, the patch gem can finally be deprecated. How should that be done? Let’s say <code>actioncable 7.0.6</code> fixes the bug. We’d change our gemspec to:</p>
<div class="highlight-wrapper group relative diff">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-nbkktzg7"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-nbkktzg7"><span class="gd">- spec.add_dependency "actioncable", ">= 7.0.4", "<= 7.0.5"
</span><span class="gi">+ spec.add_dependency "actioncable", ">= 7.0.6"
</span></code></pre>
</div>
</div>
<p>Then we’d delete the monkey patch code and replace it with this message:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-pnqwwgje"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-pnqwwgje">warn "The actioncable_redis-reconnect gem can be removed`
</code></pre>
</div>
</div>
<p>Eventually when developers update their gems, they’d make their way to the latest version of this patch gem, see the message, and remove the gem. Tada!</p>
<h2 id='conclusion' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#conclusion' aria-label='Anchor'></a>Conclusion</h2>
<p>Ideally contributions are made timely and directly into the upstream repo, but for a lot of good reasons, that’s not always possible. Monkey patching can be a great workaround, but you always want to make sure you’re managing versions with monkey patches to avoid very-difficult-to-track-down bugs in the future.</p>
/ruby-dispatch/monkey-patch-responsibly/assets/monkey-patch-thumbnail.webpSingle File Rails Appshttps://fly.io/ruby-dispatch/single-file-rails-app/2022-12-14T00:00:00+00:002023-11-21T21:08:38+00:00<div class="lead"><p>At first glance single file Rails application seems like a toy project, but they do have practical applications. Let’s take a look at how Sitepress, a semi-static site generator, uses a single page Rails application at its core for a site-generator that can both compile static websites and run inside of Rails.</p>
</div>
<p>There’s been a few interesting blog posts written about <a href='https://greg.molnar.io/blog/a-single-file-rails-application/' title=''>single file Rails applications</a>, but they all seem to stop short of describing practical use cases where you might actually ship a single page Rails application.</p>
<h2 id='problem-i-need-a-lightweight-cms-in-a-rails-application' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#problem-i-need-a-lightweight-cms-in-a-rails-application' aria-label='Anchor'></a>Problem: I need a lightweight CMS in a Rails application</h2>
<p>A few years ago I was cleaning up a large Rails application. Part of that clean-up job was consolidating all of the different ways content was being managed around the application. The most common thing I’ve seen is a controller created per page, then that controller’s <code>#show</code> action being pointed towards that one content page. This is a very common pattern in small Rails applications, and it gets tedious fast.</p>
<p>At the same time, I was building static websites with <a href='https://www.middlemanapp.com/' title=''>Middleman</a>, which had fantastic APIs like Sitemap, Frontmatter, and various view helpers that made building websites fun. How could I get that inside Rails?</p>
<p>The obvious thing to do is try adding the <code>middleman</code> gem to Rails, but I found out quickly that wasn’t going to work.</p>
<h2 id='before-a-gem-is-born' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#before-a-gem-is-born' aria-label='Anchor'></a>Before A Gem Is Born</h2>
<p>The first thing I do before I write any code for a gem is write down a few basic requirement.</p>
<p>Here’s what I needed for my project:</p>
<ol>
<li>Middleman features, like Sitemap and Frontmatter, that work in Rails.
</li><li>Must work from within an existing Rails application.
</li><li>Bonus if I can run a compilation command that emits static HTML, CSS, and JavaScript files.
</li></ol>
<p>Once I have my requirements, I start looking around to see if something already exists that satisfy them.</p>
<p>The closest project I found that could be embedded in Rails was <a href='https://github.com/thoughtbot/high_voltage' title=''>High Voltage</a>, but it lacked a Sitemap and Frontmatter. I did a few experiments where I tried embedding Middleman and Jecklly into a Rails application, but those quickly failed.</p>
<p>I quickly realized the only path forward was to create a new gem, and Sitepress was born.</p>
<h2 id='say-hello-to-sitepress' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#say-hello-to-sitepress' aria-label='Anchor'></a>Say Hello to Sitepress</h2>
<p>When Sitepress was originally built, it was a goal to have it run both inside Rails and outside of Rails. I spent most of my time getting Sitepress working great inside of Rails.</p>
<p>Getting it working outside of Rails was a pain. I wired up a rack server, used Tilt for rendering, then I needed helpers. Getting layouts working properly was full of bugs. I needed to have a decent answer for assets. The more I got into it, the more I felt like I was re-inventing a lot of Rails features, and even worse, wasting my time trying to re-integrate all of this stuff so it would work similar to Rails.</p>
<p>About the time I felt like giving up on the stand-alone version of Sitepress, I ran across a blog post entitled, “Single File Rails Applications”. That’s when the light-bulb went off in my head and I re-framed the problem for stand-alone Sitepress, “what if I can use a Rails application to run stand-alone Sitepress?”</p>
<p>If I could pull that off I’d get access to <code>tailwind-rails</code>, an asset pipeline, all its view helpers, a layout and templating system that works, and access to an entire ecosystem of plugins.</p>
<h2 id='a-look-at-sitepresss-single-file-rails-app' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#a-look-at-sitepresss-single-file-rails-app' aria-label='Anchor'></a>A look at Sitepress’s single file Rails app</h2>
<p>The fun thing about Sitepress is that its <a href='https://github.com/sitepress/sitepress/blob/main/sitepress-server/lib/sitepress/server.rb' title=''>just a Rails app</a> so you get to use all of the helpers you’re already familier with in Rails. This means if you’re into Majestic Monoliths, you can embed Sitepress in your Rails application and have a great content management system without the complexity of backing it by a database.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-52lw5n6o"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-52lw5n6o"><span class="c1">#</span>
<span class="nb">require</span> <span class="s2">"action_controller/railtie"</span>
<span class="nb">require</span> <span class="s2">"sprockets/railtie"</span>
<span class="nb">require</span> <span class="s2">"sitepress-rails"</span>
<span class="c1"># Require the gems listed in Gemfile, including any gems</span>
<span class="c1"># you've limited to :test, :development, or :production.</span>
<span class="no">Bundler</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="o">*</span><span class="no">Rails</span><span class="p">.</span><span class="nf">groups</span><span class="p">)</span>
<span class="c1"># Configure the rails application.</span>
<span class="k">module</span> <span class="nn">Sitepress</span>
<span class="k">class</span> <span class="nc">Server</span> <span class="o"><</span> <span class="no">Rails</span><span class="o">::</span><span class="no">Application</span>
<span class="c1"># Control whether or not to display friendly error reporting messages</span>
<span class="c1"># in Sitepress. The development server turns this on an handles exception,</span>
<span class="c1"># while the compile and other environments would likely have this disabled.</span>
<span class="n">config</span><span class="p">.</span><span class="nf">enable_site_error_reporting</span> <span class="o">=</span> <span class="kp">false</span>
<span class="c1"># When in a development environment, we'll want to reload the site between</span>
<span class="c1"># requests so we can see the latest changes; otherwise, load the site once</span>
<span class="c1"># and we're done.</span>
<span class="n">config</span><span class="p">.</span><span class="nf">enable_site_reloading</span> <span class="o">=</span> <span class="kp">false</span>
<span class="c1"># Default to a development environment type of configuration, which would reload the site.</span>
<span class="c1"># This gets reset later depending on a preference in the `before_initialize` callback.</span>
<span class="n">config</span><span class="p">.</span><span class="nf">eager_load</span> <span class="o">=</span> <span class="kp">true</span>
<span class="n">config</span><span class="p">.</span><span class="nf">cache_classes</span> <span class="o">=</span> <span class="kp">true</span>
<span class="n">config</span><span class="p">.</span><span class="nf">before_initialize</span> <span class="k">do</span>
<span class="c1"># Eager load classes, content, etc. to boost performance when site reloading is disabled.</span>
<span class="n">config</span><span class="p">.</span><span class="nf">eager_load</span> <span class="o">=</span> <span class="o">!</span><span class="n">config</span><span class="p">.</span><span class="nf">enable_site_reloading</span>
<span class="c1"># Cache classes for speed in production environments when site reloading is disabled.</span>
<span class="n">config</span><span class="p">.</span><span class="nf">cache_classes</span> <span class="o">=</span> <span class="o">!</span><span class="n">config</span><span class="p">.</span><span class="nf">enable_site_reloading</span>
<span class="k">end</span>
<span class="c1"># Path that points the the Sitepress UI rails app; which displays routes, error messages.</span>
<span class="c1"># etc. to the user if `enable_site_error_reporting` is enabled.</span>
<span class="n">config</span><span class="p">.</span><span class="nf">root</span> <span class="o">=</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">"../../rails"</span><span class="p">)</span>
<span class="c1"># Rails won't start without this.</span>
<span class="n">config</span><span class="p">.</span><span class="nf">secret_key_base</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">uuid</span>
<span class="c1"># Setup routes. The `constraints` key is set to `nil` so the `SiteController` can</span>
<span class="c1"># treat a page not being found as an exception, which it then handles. If the constraint</span>
<span class="c1"># was set to the default, Sitepress would hand off routing back to rails if something isn't</span>
<span class="c1"># found and fail silently.</span>
<span class="n">routes</span><span class="p">.</span><span class="nf">append</span> <span class="p">{</span> <span class="n">sitepress_pages</span> <span class="ss">root: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">controller: </span><span class="s2">"site"</span><span class="p">,</span> <span class="ss">constraints: </span><span class="kp">nil</span> <span class="p">}</span>
<span class="c1"># A logger without a formatter will crash when Sprockets is enabled.</span>
<span class="n">logger</span> <span class="o">=</span> <span class="no">ActiveSupport</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="no">STDOUT</span><span class="p">)</span>
<span class="n">logger</span><span class="p">.</span><span class="nf">formatter</span> <span class="o">=</span> <span class="n">config</span><span class="p">.</span><span class="nf">log_formatter</span>
<span class="n">config</span><span class="p">.</span><span class="nf">logger</span> <span class="o">=</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TaggedLogging</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">logger</span><span class="p">)</span>
<span class="c1"># Debug mode disables concatenation and preprocessing of assets.</span>
<span class="c1"># This option may cause significant delays in view rendering with a large</span>
<span class="c1"># number of complex assets.</span>
<span class="n">config</span><span class="p">.</span><span class="nf">assets</span><span class="p">.</span><span class="nf">debug</span> <span class="o">=</span> <span class="kp">false</span>
<span class="c1"># Suppress logger output for asset requests.</span>
<span class="n">config</span><span class="p">.</span><span class="nf">assets</span><span class="p">.</span><span class="nf">quiet</span> <span class="o">=</span> <span class="kp">true</span>
<span class="c1"># Do not fallback to assets pipeline if a precompiled asset is missed.</span>
<span class="n">config</span><span class="p">.</span><span class="nf">assets</span><span class="p">.</span><span class="nf">compile</span> <span class="o">=</span> <span class="kp">true</span>
<span class="c1"># Allow any host to connect to the development server. The actual binding is</span>
<span class="c1"># controlled by server in the `sitepress-cli`; not by Rails.</span>
<span class="n">config</span><span class="p">.</span><span class="nf">hosts</span> <span class="o"><<</span> <span class="nb">proc</span> <span class="p">{</span> <span class="kp">true</span> <span class="p">}</span> <span class="k">if</span> <span class="n">config</span><span class="p">.</span><span class="nf">respond_to?</span> <span class="ss">:hosts</span>
<span class="c1"># Stand-alone boot locations</span>
<span class="n">paths</span><span class="p">[</span><span class="s2">"config/initializers"</span><span class="p">]</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">"./config/initializers"</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>This is a big win for individuals or small teams who want to publish a few landing pages, some help pages, and terms of service, etc. and as Fly has shown, it’s pretty straight forward to get this running quickly around the world, close to your customers, without adding the latency of a CDN to your application.</p>
<h2 id='when-do-single-file-rails-applications-make-sense' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#when-do-single-file-rails-applications-make-sense' aria-label='Anchor'></a>When Do Single File Rails Applications Make Sense?</h2>
<p>Single page Rails applications shine when you need to deploy Rails applications to a bunch of workstations, as opposed to deploying them to your servers.</p>
<ul>
<li><p><strong class='font-[550] text-navy-950'>Preview servers for web content</strong> - Static site builders, like Sitepress, use single page Ruby applications to boot a preview server that developers can use to preview their changes.</p>
</li><li><p><strong class='font-[550] text-navy-950'>Local Web UI that manages local services</strong> - Maybe you need to build a GUI that will run on a server in a data center or a workstation that’s accessible remotely, but you don’t want all the “heft” of a full blown rails app.</p>
</li><li><p><strong class='font-[550] text-navy-950'>Server-Side Rendered site</strong> - Static websites are fast and have low operational complexity, but maybe you want to do a few dynamic things on the server like localize content for people. <a href='/ruby-dispatch/semi-static-websites/' title=''>A semi-static website</a> can help that’s running on a small Rails app.</p>
</li><li><p><strong class='font-[550] text-navy-950'>Test cases</strong> - When reporting a bug in Rails core, it’s helpful to isolate the exact Rails gems and configuration to help a maintain reproduce a bug. This approach can save a lot of time for the maintainer and increase the liklehood that your issue will get fixed.</p>
</li></ul>
<p>This shortlist will hopefully give you a few practical ideas that you can add to your toolbox for developing Rails applications.</p>
/ruby-dispatch/single-file-rails-app/assets/single-file-rails-app-thumbnail.webpDockerfile-less-deployshttps://fly.io/ruby-dispatch/dockerfile-less-deploys/2022-12-07T00:00:00+00:002023-11-21T21:08:38+00:00<div class="lead"><p><a href="http://fly.io/" title="">Fly.io</a> is a great place to run Rails applications, especially if you plan on running them on multiple servers around the world so your users have a fast, snappy, low-latency experience.
<a href="https://fly.io/docs/rails/getting-started/" title="">Give us a whirl</a> and get up and running quickly.</p>
</div><h2 id='introduction' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#introduction' aria-label='Anchor'></a>Introduction</h2>
<p>First, let’s talk about why we decided to use <a href='https://opencontainers.org/' title=''>OCI</a> container images in the first place. In short, containers are freaking awesome. They allow us to package up our applications and all of their dependencies into a single, self-contained unit that can be easily extracted, deployed, and run on any machine. This makes it super easy for our users to get up and running with Fly.io, without having to worry about all the little details of setting up their environment.</p>
<p>This naturally leads to a baseline approach where every framework uses a <a href='https://docs.docker.com/engine/reference/builder/' title=''>Dockerfile</a> and a <a href='https://toml.io/en/' title=''>TOML</a> file. This is great for system administrators, polyglots, and Rails developers who are comfortable with Dockerfiles. What that leaves behind is Rails developers who spend most of their time in an IDE on Macs or Windows; which frankly is most of them. Many of which have been spoiled by Heroku for the past 10 years.</p>
<p>A desire to avoid Dockerfiles has lead many to prefer to use buildpacks, nixpacks or other alternatives, and when they have problems with those approaches instead of reporting the problems to the maintainers of these alternatives they report the problem to us.</p>
<p>Below is a proof of concept of an alternative approach where from a Fly.io platform point of view everything is Dockerfiles and TOML files and from a developer point of view everything is Rails and Ruby, giving us the best of both worlds.</p>
<p>This is not a radical change. <code>flyctl</code> will already build you an initial Dockerfile that meets many needs. This merely takes that approach further by dynamically generating a custom tailored Dockerfile on <em>every</em> Deploy.</p>
<p>What it does mean is that the Dockerfile needs to be correct and complete every time. No more relying on webpages of instructions. Fortunately <a href='http://whatisthor.com/' title=''>Thor</a> and <a href='https://github.com/ruby/erb#erb' title=''>ERB</a> are good at this.</p>
<p>In order to run this make sure you have flyctl version v0.0.433 or later as this is when support was added for dockerignore files to be provided at deploy time.</p>
<figure class="post-cta">
<figcaption>
<h1>You can play with this right now.</h1>
<p>It’ll take less than 10 minutes to get your Rails application running globally.</p>
<a class="button bg-white text-violet-500 hover:bg-opacity-75 hover:text-violet-700 transition-colors" href="https://fly.io/docs/rails/">
Try Fly for free <span class='opacity-50'>→</span>
</a>
</figcaption>
<div class="image-container">
<img src="/static/images/cta-dog.webp" srcset="/static/images/cta-dog@2x.webp 2x" alt="">
</div>
</figure>
<hr>
<h2 id='part-one-a-simple-visitor-counter' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#part-one-a-simple-visitor-counter' aria-label='Anchor'></a>Part one, a simple visitor counter</h2>
<p>Start by creating a simple application and scaffold a visitor counter table:</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-u0cmzjb1"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-u0cmzjb1">rails new welcome <span class="nt">--css</span> tailwind
<span class="nb">cd </span>welcome
git add <span class="nb">.</span>
git commit <span class="nt">-a</span> <span class="nt">-m</span> <span class="s1">'initial commit'</span>
bin/rails generate scaffold visitor counter:integer
bin/rails db:migrate
</code></pre>
</div>
</div>
<p>Modify the index method in the visitor controller to find the counter and increment it.</p>
<p>Edit <code>app/controllers/visitors_controller.rb</code>:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-zjk76wn1"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-zjk76wn1"> <span class="c1"># GET /visitors or /visitors.json</span>
<span class="k">def</span> <span class="nf">index</span>
<span class="vi">@visitor</span> <span class="o">=</span> <span class="no">Visitor</span><span class="p">.</span><span class="nf">find_or_create_by</span><span class="p">(</span><span class="ss">id: </span><span class="mi">1</span><span class="p">)</span>
<span class="vi">@visitor</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span>
<span class="ss">counter: </span><span class="p">(</span><span class="vi">@visitor</span><span class="p">.</span><span class="nf">counter</span> <span class="o">||</span> <span class="mi">0</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span>
<span class="p">)</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Change the index view to show the fly.io balloon and the counter.</p>
<p>Replace <code>app/views/visitors/index.html.erb</code> with:</p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-cev5flyt"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-cev5flyt"><span class="nt"><div</span> <span class="na">class=</span><span class="s">"absolute top-0 left-0 h-screen w-screen mx-auto mb-3 bg-navy px-20 py-14 rounded-[20vh] flex flex-row items-center justify-center"</span> <span class="na">style=</span><span class="s">"background-color:rgb(36 24 91)"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"https://fly.io/static/images/brand/brandmark-light.svg"</span> <span class="na">class=</span><span class="s">"h-[50vh]"</span> <span class="na">style=</span><span class="s">"margin-top: -15px"</span> <span class="na">alt=</span><span class="s">"The monochrome white Fly.io brandmark on a navy background"</span> <span class="na">srcset=</span><span class="s">""</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"text-white"</span> <span class="na">style=</span><span class="s">"font-size: 40vh; padding: 10vh"</span> <span class="na">data-controller=</span><span class="s">"counter"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="vi">@visitor</span><span class="p">.</span><span class="nf">counter</span><span class="p">.</span><span class="nf">to_i</span> <span class="cp">%></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
</code></pre>
</div>
</div>
<p>Define the root path to be the visitors index page:</p>
<p>Edit <code>config/routes.rb</code>:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-9bldgejf"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-9bldgejf"> <span class="c1"># Defines the root path route ("/")</span>
<span class="n">root</span> <span class="s1">'visitors#index'</span>
</code></pre>
</div>
</div>
<p>Save our work so we can see what changed later.</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-hvdb7ovt"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-hvdb7ovt">git add <span class="nb">.</span>
git commit <span class="nt">-m</span> <span class="s1">'initial application'</span>
</code></pre>
</div>
</div>
<p>Now let’s do our first deployment. If desired add <code>—name</code> and <code>—org</code> options to the <code>generate</code> command below, or let them default:</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-bjys4v70"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-bjys4v70">bundle add fly-rails
bin/rails generate fly:app
bin/rails fly:deploy
</code></pre>
</div>
</div>
<p>Note that a volume is created. That’s to store the <a href='https://www.sqlite.org/index.html' title=''>sqlite3</a> database. Making that work actually takes multiple steps: create a <a href='https://fly.io/docs/reference/volumes/' title=''>volume</a>, mount the volume, and set an environment variable to cause Rails to put the database on the mounted volume.</p>
<p>All of that is taken care of for you.</p>
<p>To see your app in production, run <code>fly open</code>.</p>
<hr>
<h2 id='part-two-change-the-database-to-postgresql' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#part-two-change-the-database-to-postgresql' aria-label='Anchor'></a>Part two: change the database to PostgreSQL</h2>
<p>While sqlite3 is more than adequate for this silly example, many applications require something more. Let’s switch to <a href='https://www.postgresql.org/' title=''>postgresql</a>.</p>
<p>Edit <code>config/database.yml</code>:</p>
<div class="highlight-wrapper group relative yaml">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-b2irdv10"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-b2irdv10"><span class="na">production</span><span class="pi">:</span>
<span class="na">adapter</span><span class="pi">:</span> <span class="s">postgresql</span>
</code></pre>
</div>
</div>
<p>Deploy your change:</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-etxfv83b"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-etxfv83b">bin/rails fly:deploy
</code></pre>
</div>
</div>
<p>At this point, a <code>pg</code> gem is installed, a <code>PostgreSQL</code> database is created, and a <a href='https://fly.io/docs/reference/secrets/' title=''>secret</a> is set. Also, there now is a separate <a href='https://fly.io/docs/reference/configuration/#the-deploy-section' title=''>release</a> step that will run your database migrations before restarting your server.</p>
<p>Again, all without you having to worry about anything.</p>
<hr>
<h2 id='part-three-update-the-counter-without-requiring-a-refresh' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#part-three-update-the-counter-without-requiring-a-refresh' aria-label='Anchor'></a>Part three: update the counter without requiring a refresh</h2>
<p>Sending asynchronous updates requires a <a href='https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API' title=''>WebSocket</a> as well as an ability to process updates in the background. Rails makes this easy.</p>
<p>Start by generating a new <a href='https://guides.rubyonrails.org/action_cable_overview.html' title=''>Action Cable</a> channel:</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-yjgaikmk"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-yjgaikmk">bin/rails generate channel counter
</code></pre>
</div>
</div>
<p>Make a partial that puts the counter into a <a href='https://turbo.hotwired.dev/handbook/frames' title=''>Turbo Frame.</a></p>
<p>Create <code>app/views/visitors/_counter.html.erb</code>:</p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-rzx8e01w"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-rzx8e01w"><span class="cp"><%=</span> <span class="n">turbo_frame_tag</span><span class="p">(</span><span class="n">dom_id</span> <span class="n">visitor</span><span class="p">)</span> <span class="k">do</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">visitor</span><span class="p">.</span><span class="nf">counter</span><span class="p">.</span><span class="nf">to_i</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre>
</div>
</div>
<p>Update the view to add <code>turbo_stream_from</code> and render the partial.</p>
<p>Update <code>app/views/visitors/index.html.erb</code>:</p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-fbf5gp18"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-fbf5gp18"><span class="cp"><%=</span> <span class="n">turbo_stream_from</span> <span class="s1">'counter'</span> <span class="cp">%></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"absolute top-0 left-0 h-screen w-screen mx-auto mb-3 bg-navy px-20 py-14 rounded-[20vh] flex flex-row items-center justify-center"</span> <span class="na">style=</span><span class="s">"background-color:rgb(36 24 91)"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"https://fly.io/static/images/brand/brandmark-light.svg"</span> <span class="na">class=</span><span class="s">"h-[50vh]"</span> <span class="na">style=</span><span class="s">"margin-top: -15px"</span> <span class="na">alt=</span><span class="s">"The monochrome white Fly.io brandmark on a navy background"</span> <span class="na">srcset=</span><span class="s">""</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"text-white"</span> <span class="na">style=</span><span class="s">"font-size: 40vh; padding: 10vh"</span> <span class="na">data-controller=</span><span class="s">"counter"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="s2">"counter"</span><span class="p">,</span> <span class="ss">visitor: </span><span class="vi">@visitor</span> <span class="cp">%></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
</code></pre>
</div>
</div>
<p>Add <code>broadcast_replace_later</code> to the controller.</p>
<p>Edit <code>app/controllers/visitors_controller.rb</code>:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-wkzp1p6c"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-wkzp1p6c"> <span class="c1"># GET /visitors or /visitors.json</span>
<span class="k">def</span> <span class="nf">index</span>
<span class="vi">@visitor</span> <span class="o">=</span> <span class="no">Visitor</span><span class="p">.</span><span class="nf">find_or_create_by</span><span class="p">(</span><span class="ss">id: </span><span class="mi">1</span><span class="p">)</span>
<span class="vi">@visitor</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span>
<span class="ss">counter: </span><span class="p">(</span><span class="vi">@visitor</span><span class="p">.</span><span class="nf">counter</span> <span class="o">||</span> <span class="mi">0</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span>
<span class="p">)</span>
<span class="vi">@visitor</span><span class="p">.</span><span class="nf">broadcast_replace_later_to</span> <span class="s1">'counter'</span><span class="p">,</span>
<span class="ss">partial: </span><span class="s1">'visitors/counter'</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>Deploy your change:</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-74qfw90h"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-74qfw90h">bin/rails fly:deploy
</code></pre>
</div>
</div>
<p>At this point, a <code>redis</code> gem is installed (if it wasn’t already), an <a href='https://fly.io/docs/reference/redis/' title=''>Upstash Redis</a> cluster is created if your organization didn’t already have one (otherwise that cluster is reused), and a secret is set.</p>
<p>Once again, all without you having to worry about anything.</p>
<hr>
<h2 id='part-four-change-your-cable-adapter-to-any_cable' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#part-four-change-your-cable-adapter-to-any_cable' aria-label='Anchor'></a>Part four: change your cable adapter to any_cable</h2>
<p>We’ve tried out two different databases. Let’s try out <a href='https://anycable.io/' title=''>AnyCable</a> as an alternate cable implementation.</p>
<p>Modify <code>config/cable.yml</code>:</p>
<div class="highlight-wrapper group relative yaml">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-rtytt975"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-rtytt975"><span class="na">production</span><span class="pi">:</span>
<span class="na">adapter</span><span class="pi">:</span> <span class="s">any_cable</span>
</code></pre>
</div>
</div>
<p>Deploy your change:</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-47007ya1"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-47007ya1">bin/rails fly:deploy
</code></pre>
</div>
</div>
<p>Note that this time you are likely to see <code>502 Bad Gateway</code>. That’s because <a href='https://www.nginx.com/' title=''>nginx</a> typically starts faster than Rails and at this point this is just a demo. Don’t worry, Rails will start in a few seconds and things will work once it starts. If you check the logs you will often see a similar problem where anycable go starts faster than anycable rpc, but that also corrects itself.</p>
<p>Once again, gems are installed and this time at runtime multiple processes are run, including one additional process (nginx) to transparently route the websocket to anycable. All on a single 256MB fly machine. The details are messy, but you don’t have to worry about them.</p>
<hr>
<h2 id='recap' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#recap' aria-label='Anchor'></a>Recap</h2>
<p>We’ve deployed four different configurations without having to touch
anything but Rails files.</p>
<p>Run the following command to see what files were modified:</p>
<div class="highlight-wrapper group relative bash">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-96l80oj3"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-96l80oj3">git status
</code></pre>
</div>
</div>
<p>In addition to the <code>config</code> and <code>app</code> files that you modified you should see two files:</p>
<ul>
<li><code>config/fly.rb</code>
</li><li><code>fly.toml</code>
</li></ul>
<p>Both are relatively small, in fact <code>fly.toml</code> is only one line. The other file is likely to change dramatically so don’t get too attached to it. What it is meant to describe is the deployment specific information that can’t be gleaned from the configuration files alone, things like machine and volume sizes. The hope is that it will cover replication and geographic placement of machines; conceptually similar to what terraform provides today but expressed at a much higher level and in a familiar Ruby syntax.</p>
<p>If you want to see the configuration files that actually are used, run the following command:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-bctdhe98"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-bctdhe98">bin/rails generate fly:app --eject
</code></pre>
</div>
</div>
<p>Note: this demo uses fly machines v2, and requires a script (<code>rails deploy</code>) to build a Dockerfile and run the underlying commands and APIs to create machines, set secrets, etc. It is possible to run with nomad (a.k.a. v1) by passing <code>--nomad</code> on the <code>bin/rails generate fly:app</code> command, and while this will allow you to run vanilla <code>fly deploy</code> the trade off is this is accomplished by creating a <code>Dockerfile</code> and various other artifacts.</p>
<hr>
<h2 id='futures' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#futures' aria-label='Anchor'></a>Futures</h2>
<p>Some examples of things worth exploring</p>
<ul>
<li>Not implemented yet, but it should be possible to modify the size of a volume in <code>config/fly.rb</code> and deploy to make a change.
</li><li>While the above demo made use of fly’s Postgres offering, you may very well want a managed alternative. Or go the other way and run Debian’s Postgres within the same VM. Or go with a different database entirely. All should be easy as setting some <a href='https://edgeguides.rubyonrails.org/security.html' title=''>custom credentials</a>.
</li><li><a href='https://edgeguides.rubyonrails.org/active_storage_overview.html' title=''>Active Storage</a> supports a number of back-ends including Amazon’s S3 and Google’s Cloud. Let’s make that easy too.
</li><li>The example above ran AnyCable in the same VM which may not be optimal for scaling reasons. Not to mention that taking AnyCable down every time you deploy a change to your application will drop sessions. We should make it easy to say that I want two of this application running in this region and three of that application running in this other region.
</li><li>Currently <code>fly deploy</code> can be run as a GitHub action. Extend this work to cover <code>bin/rails fly:deploy</code>.
</li><li>Consider adding other <code>bin/rails fly:</code> tasks that add value. Perhaps one that directly runs <code>rails console</code> on the deployed machine. Perhaps another that simply sets the working directory properly on ssh.
</li></ul>
<p>The common theme of all of the above is to look for a higher level abstraction than what is currently provided in Dockerfiles and fly.toml, while retaining the ability to drop down and say things like “also install this Debian package” or “also expose this port”.</p>
<p>Feedback is welcome on <a href='https://community.fly.io/t/dockerfile-less-deploys-rails-demo/9234/1' title=''>community.fly.io</a>.</p>
<p>Oh, and it probably is worth mentioning that the source to the fly-ruby gem is <a href='https://github.com/superfly/fly-rails' title=''>on GitHub</a> and written in Ruby. Pull requests welcome!</p>
/ruby-dispatch/dockerfile-less-deploys/assets/gift-wrapping-thumbnail.webpRails Background Jobs with Fly Machineshttps://fly.io/ruby-dispatch/rails-background-jobs-with-fly-machines/2022-10-05T00:00:00+00:002023-11-21T21:08:38+00:00<p><strong class='font-[550] text-navy-950'>Fly Machines can boot a VM in 500ms, run a Rails background job, then turn off when it’s done. That means you don’t have to pay for a server to sit idle if there’s no jobs to process <em>and</em> you can have a much more scalable pool of on-demand workers when your application starts to get busy.</strong></p>
<p>When a Rails application needs to do some heavy lifting like processing lots of data or making calculation that takes a long time, a common approach is to spin up an ActiveJob and asynchronously run it in a background worker.</p>
<p>According to <a href='https://www.ruby-toolbox.com/categories/Background_Jobs' title=''>The Ruby Toolbox</a>, the <a href='https://www.ruby-toolbox.com/categories/Background_Jobs' title=''>most popular background job framework</a> for Rails is <a href='https://www.ruby-toolbox.com/projects/sidekiq' title=''>Sidekiq</a>. Sidekiq is a great framework for creating background jobs, managing retries when they fail and queue priorities/topologies, and monitoring their status from the application or admin panel.</p>
<p>Like many background workers, Sidekiq runs in a separate process from the Rails application, which requires additional CPU and memory resources. That’s all great when there’s jobs to process, but it’s not fun paying for a server that sits around doing nothing when there’s no jobs on the queue to process.</p>
<p>Additionally, when a bunch of jobs start rolling in that need to be processed, it would be great if the pool of workers could scale up temporarily to handle the increased load. Then scale back down when things are less busy.</p>
<p>Fly Machines could solve that problem by having zero background workers or processes running if there’s no jobs. When a job comes in, the Rails ActiveJob processor spins up a Fly Machine to process it. Here’s what that looks like as a really basic ActiveJob adapter.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-7vqau6ao"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-7vqau6ao"><span class="k">module</span> <span class="nn">ActiveJob</span>
<span class="k">module</span> <span class="nn">QueueAdapters</span>
<span class="c1"># == Fly Machine adapter for Active Job</span>
<span class="c1">#</span>
<span class="c1"># Boot VMs in 500ms, run a Rails background job, and shuts it down when it's all done.</span>
<span class="c1">#</span>
<span class="c1"># Rails.application.config.active_job.queue_adapter = :fly_machine</span>
<span class="k">class</span> <span class="nc">FlyMachineAdapter</span>
<span class="k">def</span> <span class="nf">enqueue</span><span class="p">(</span><span class="n">job</span><span class="p">)</span> <span class="c1"># :nodoc:</span>
<span class="no">Fly</span><span class="p">.</span><span class="nf">app</span><span class="p">.</span><span class="nf">machine</span><span class="p">.</span><span class="nf">fork</span> <span class="ss">init: </span><span class="p">{</span>
<span class="ss">cmd: </span><span class="p">[</span>
<span class="s2">"/app/bin/rails"</span><span class="p">,</span>
<span class="s2">"runner"</span><span class="p">,</span>
<span class="s2">"ActiveJob::Base.deserialize(</span><span class="si">#{</span><span class="n">job</span><span class="p">.</span><span class="nf">serialize</span><span class="si">}</span><span class="s2">).run"</span>
<span class="p">]</span>
<span class="p">}</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">enqueue_at</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="c1"># :nodoc:</span>
<span class="k">raise</span> <span class="no">NotImplementedError</span><span class="p">,</span> <span class="s2">"Does not yet support queueing background jobs in the future"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>When the job is done and exits, the Fly Machine also shuts down. If the machine took 3 seconds to run, you pay for 3 seconds of Fly Machine time. The important part is that you do <em>not</em> have to keep paying for resources when there’s no jobs on the queue.</p>
<p>The <a href='https://github.com//fly-apps/rails-machine-workers/blob/main/app/models/fly/machine.rb#L32-L42' title=''><code>Fly.app.machine.fork</code></a> looks like magic, but it’s not. All it does is gets the image name and <code>ENV</code> of the currently running Rails application and stuffs it into a <a href='https://fly.io/docs/reference/machines/' title=''>Fly Machines API</a> call that boots a Firecracker VM. It’s <em>almost</em> like forking, close enough to call it a fork for purposes of this demo.</p>
<p>You can see the source code at <a href='https://github.com/fly-apps/rails-machine-workers' title=''>https://github.com/fly-apps/rails-machine-workers</a> and the running application at <a href='https://rails-machine-workers.fly.dev' title=''>https://rails-machine-workers.fly.dev</a>, which includes a really big security warning you should know about if you try doing this today.</p>
<p>But wait, there’s more!</p>
<h2 id='embracing-the-monolith' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#embracing-the-monolith' aria-label='Anchor'></a>Embracing the monolith</h2>
<p>It’s tempting to reach for edge function services from <code>$BIG_CLOUD_COMPANY</code>, but those usually require managing functions as a “separate thing” from the Rails app, which means as a developer you have to build it differently and manage it differently, like deploying the functions separately from the application code. There’s tooling that makes this a little easier, but it’s still another thing you’d have to worry about.</p>
<p>When using Fly Machines for background workers, you don’t have to do anything special—you just treat your background jobs as background jobs and there’s nothing extra to manage once you get it all setup. This makes the <code>fly deploy</code> command <em>even more powerful</em>, which is insane because the thing can already deploy your single application to a fleet of servers around the world. Now it can also deploy your background workers. 🤯</p>
<h2 id='limitations-to-this-proof-of-concept' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#limitations-to-this-proof-of-concept' aria-label='Anchor'></a>Limitations to this proof-of-concept</h2>
<p>This Fly Machines background worker proof-of-concept is still very basic and comes with a few issues depending on the needs of your application.</p>
<h3 id='lower-processing-latency' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#lower-processing-latency' aria-label='Anchor'></a>Lower processing latency</h3>
<p>The basic concept of “fire-and-forget-a-background-job-into-a-machine” comes with latency that may or may not be acceptable for your application. Here’s what the back-of-napkin math looks like:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-bc0xh5ca"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-bc0xh5ca"> Fly Machine Boot Time 500ms
+ Rails Boot Time 1500ms..5000ms+
--------------------------------------
Total latency 2000ms..5500ms+
</code></pre>
</div>
</div>
<p>This latency could be negligible if the background job takes a few minutes to complete, but if the background job should only take a half a second, it doesn’t make sense to spend five seconds booting up to do it.</p>
<p>This problem is very solvable. The most straight forward way would be dumping the background job into a queue, boot the machine, the machine phones back home to the Rails app “I’m alive and processing jobs”. The machine then proceeds to process all jobs on the queue until its empty. When the queue is empty the worker could either be configured to shutdown right away or wait for a configurable amount of time for another job before shutting down.</p>
<p>Here’s what that might look like:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-o1n7zbv8"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-o1n7zbv8">./bin/fly-machine-worker --wait 600
</code></pre>
</div>
</div>
<p>If the value of <code>600</code> is passed, the worker would wait for 600 seconds after the last job was processed before terminating. The first job would have to boot the Rails application, but subsequent jobs would get picked up by the already booted VM. When things slow down for 10 minutes, the work would terminate.</p>
<p>To guarantee one worker is always running so there’s no wait time, <code>forever</code> could be passed to the worker.</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-s4xkh5nr"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-s4xkh5nr">./bin/fly-machine-worker --wait forever
</code></pre>
</div>
</div><h3 id='handle-retries-for-jobs-that-fail' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#handle-retries-for-jobs-that-fail' aria-label='Anchor'></a>Handle retries for jobs that fail</h3>
<p>If a Fly Machine jobs fails, the work wouldn’t retry because the state of the job isn’t being persisted anywhere and updated. It would be great to track the state of a job and the current attempts so they could be retried.</p>
<h3 id='too-many-workers' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#too-many-workers' aria-label='Anchor'></a>Too many workers</h3>
<p>The funny thing about this approach is the problem of <em>too many workers</em> spinning up and potentially bringing down your application. Let’s say somebody loaded up your application with a gazillion requests that spin up a gazillion background jobs. If those jobs are competing for the same resource in your application, like trying to write to the same row in a database, your application could start to have some problems.</p>
<p>There’s lots of approaches to solving these problems. One way is to name queues and set limits to the number of workers that can be spun up per queue. This approach has many precedents in a lot of Rails existing job queue frameworks.</p>
<h2 id='activejob-might-not-be-the-right-place-to-solve-these-problems' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#activejob-might-not-be-the-right-place-to-solve-these-problems' aria-label='Anchor'></a>ActiveJob might not be the right place to solve these problems</h2>
<p>The list of limitations above have mostly been solved by all the great <a href='https://www.ruby-toolbox.com/categories/Background_Jobs' title=''>background job</a> frameworks in Ruby. It might make more sense to perform the “Fly Machine Fork” from within a worker process itself.</p>
<p>Does this mean you’d then be back to paying for a worker that sits mostly idle monitoring a queue? Not necessarily. <a href='https://github.com/mperham/sidekiq/wiki/Embedding' title=''>Sidekiq 7.0 introduces a concept called “embedding”</a>, which is another way of saying Puma creates a Sidekiq worker process from within the Rails server. If an embedded work is monitoring a queue and sitting mostly idle, you don’t care as much because you’re not paying for a dedicated server. When a job comes rolling in, Sidekiq would kick off a job in a new Fly Machine, getting back to the world where you only have to pay for what you use.</p>
<h2 id='quot-compress-the-complexity-of-modern-web-apps-quot' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#quot-compress-the-complexity-of-modern-web-apps-quot' aria-label='Anchor'></a>“Compress the complexity of modern web apps”</h2>
<p>The ability to “fork” a Rails app on an entirely different machine without working about managing “cloud functions” is valuable to developers because they can spend more time worrying about their application and less time worrying about “managing stuff”.</p>
<p>It makes the monolith even more majestic since you don’t have to worry as much about capacity planning for your background jobs and can think about doing some really hefty work with the full might of Fly Machines at your disposal.</p>
<p>The approach still needs a lot of work before its production ready, but the basic building blocks are there to make Rails applications do even more incredible things.</p>
<h2 id='additional-resources' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#additional-resources' aria-label='Anchor'></a>Additional resources</h2>
<ul>
<li><a href='https://github.com/fly-apps/rails-machine-workers' title=''>https://github.com/fly-apps/rails-machine-workers</a> -Source code of Rails Background workers
</li><li><a href='https://rails-machine-workers.fly.dev' title=''>https://rails-machine-workers.fly.dev</a> - Website that goes through the step-by-step of how to setup Fly background workers
</li><li><a href='https://fly.io/docs/rails/advanced-guides/machine/' title=''>https://fly.io/docs/rails/advanced-guides/machine/</a> - API docs for Fly Machines
</li><li><a href='https://fly.io/docs/reference/machines/' title=''>https://fly.io/docs/reference/machines/</a> - <code>flyctl machine</code> docs.
</li></ul>
/ruby-dispatch/rails-background-jobs-with-fly-machines/assets/taffy-robots-thumbnail.webpUsing TurboStream with the Fetch APIhttps://fly.io/ruby-dispatch/turbostream-fetch/2022-09-08T00:00:00+00:002023-11-21T21:08:38+00:00<div class="lead"><p><a href="http://fly.io/" title="">Fly.io</a> runs apps close to users around the world. This enables highly dynamic interactive forms, even in cases where a server interaction is required. <a href="https://fly.io/docs/speedrun/" title="">Give us a whirl</a> and get up and running quickly.</p>
</div>
<p>Many people see Rails as a framework that will get you to IPO and beyond. I, personally, I’m more interested in the long tail of applications that are used by only a small group of people.</p>
<p>I wrote one such application to schedule heats for ballroom competitions, which I named <a href='https://github.com/rubys/showcase#showcase' title=''>Showcase</a>. That’s heats, like in a swim match, but where the women’s outfits are sequined and men may be wearing tails. One key difference: ballroom dances are performed by pairs of participants, which complicates scheduling as one person cannot be in two places at the same time.</p>
<p>Throughput isn’t a concern for the Showcase application, but latency is. If you are entering a large amount of data (in human terms, not in “big data” terms), you don’t want to wait for requests to be processed. Therefore server responses for trivial requests need to be seen as instantaneous, which is anything under 100ms.</p>
<p>The remainder of this post focuses on how forms in the Showcase application are made dynamic using <a href='https://hotwired.dev/' title=''>Hotwire</a>, combining a number of techniques that may not be obvious.</p>
<h2 id='forms-that-change' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#forms-that-change' aria-label='Anchor'></a>Forms That Change</h2>
<p>Forms can change based on user input for any number of reasons: drag and drop; selecting values from a drop-down menu; even submitting the form can cause new form fields to appear, disappear, or change. I’ll use a New Participants form to demonstrate:</p>
<p><img alt="New Participant/Guest form with inputs for Name, Studio, Type, and Package. Type is set to Guest. There are two buttons: Create Person, and Back to People." src="/images/new-guest-form.webp" /></p>
<p>Not shown above are a number of hidden fields. If the participant’s type is Professional, additional fields will be added to the form to indicate whether the participant is a Leader or Follower (or both!). Students have additional fields including Level and Age categories, and even have a way to request that they not be scheduled at the same time as a friend or spouse so that they can video each other. Leaders have one additional form field where a back number can be entered.</p>
<p>A form with all of the input fields revealed looks like the following:</p>
<p><img alt="New Participant form as above, but with Type set to Student and additional fields for Level, Age, Role, Back Number, and Avoid scheduling the same time as." src="/images/new-student-form.webp" /></p>
<p>Hiding and revealing portions of the form is relatively straightforward, oftentimes it is as simple as placing lines of code like the the following in a <a href='https://stimulus.hotwired.dev/' title=''>Stimulus</a> controller:</p>
<div class="highlight-wrapper group relative javascript">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-awkv92a0"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-awkv92a0"><span class="k">this</span><span class="p">.</span><span class="nx">levelTarget</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="dl">'</span><span class="s1">hidden</span><span class="dl">'</span><span class="p">);</span>
</code></pre>
</div>
</div>
<p>Now I’d like to draw your attention to the the Package field in this form. It turns out that the list of packages to choose from depends on both the Studio and Type of the person being added or edited. The server knows how to construct the Package selection from that information.</p>
<p>A common approach to solving problems such as these is a <a href='https://en.wikipedia.org/wiki/Single-page_application' title=''>single page application</a> which would involve client-side rendering of this part of the page, initially using either hydration on the client, possibly with server-side rendering (SSR).</p>
<p>The Hotwire approach is different. The content is placed into a <a href='https://turbo.hotwired.dev/handbook/frames' title=''>Turbo Frame</a> and rendered on the server. The only role the client has in the process is to replace an element in the DOM with the new content.</p>
<p>Now lets look at how this is put together, starting with the HTML…</p>
<h2 id='initial-render-of-the-form' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#initial-render-of-the-form' aria-label='Anchor'></a>Initial Render of the Form</h2>
<p>The Showcase application makes use of both Stimulus and Turbo Frames.</p>
<p>Stimulus enables you to attach JavaScript controllers and actions to your HTML via <code>data-</code> attributes. Turbo Frames encourages you to split out content that may be replaced later into a <a href='https://guides.rubyonrails.org/layouts_and_rendering.html#using-partials' title=''>partial</a>. Both approaches are employed here.</p>
<p>Focusing on the portions of HTML that are related to the need to show the correct list of packages given the selected Studio and Type leaves the following portions of the form template, <code>_form.html.erb</code>:</p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-x3y5chn8"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-x3y5chn8"><span class="cp"><%=</span> <span class="n">form_with</span><span class="p">(</span><span class="ss">model: </span><span class="n">person</span><span class="p">,</span> <span class="ss">data: </span><span class="p">{</span>
<span class="ss">controller: </span><span class="s2">"person"</span><span class="p">,</span>
<span class="ss">id: </span><span class="n">person</span><span class="p">.</span><span class="nf">id</span>
<span class="p">})</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%></span>
...
<span class="nt"><div></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:type</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">select</span> <span class="ss">:type</span><span class="p">,</span> <span class="vi">@types</span><span class="p">,</span> <span class="p">{},</span>
<span class="s1">'data-person-target'</span> <span class="o">=></span> <span class="s1">'type'</span><span class="p">,</span>
<span class="s1">'data-action'</span> <span class="o">=></span> <span class="s1">'person#setType'</span><span class="p">,</span>
<span class="s1">'data-url'</span> <span class="o">=></span> <span class="n">type_people_path</span> <span class="cp">%></span>
<span class="nt"></div></span>
...
<span class="cp"><%=</span> <span class="n">render</span> <span class="ss">partial: </span><span class="s1">'package'</span><span class="p">,</span> <span class="ss">locals: </span><span class="p">{</span> <span class="ss">person: </span><span class="n">person</span> <span class="p">}</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre>
</div>
</div>
<p>Note the <code>data-url</code> attribute which identifies the route where POST requests will be sent.</p>
<p>And now, <code>_package.html.erb</code> which is a reusable partial enabling it to be both rendered within the initial form and later rendered separately:</p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-schpaxu6"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-schpaxu6"><span class="cp"><%=</span> <span class="n">turbo_frame_tag</span><span class="p">(</span><span class="s1">'package-select'</span><span class="p">)</span> <span class="k">do</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">unless</span> <span class="vi">@packages</span><span class="p">.</span><span class="nf">empty?</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">label_tag</span> <span class="ss">:person_package_id</span><span class="p">,</span> <span class="s1">'Package'</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">select_tag</span> <span class="s1">'person[package_id]'</span><span class="p">,</span>
<span class="n">options_for_select</span><span class="p">(</span><span class="vi">@packages</span><span class="p">,</span> <span class="vi">@person</span><span class="p">.</span><span class="nf">package_id</span> <span class="o">||</span> <span class="s1">''</span><span class="p">)</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre>
</div>
</div>
<p>Now that you have seen how the initial content is rendered, lets move on to how the frame gets replaced as the user makes their selections.</p>
<h2 id='fetching-new-content-on-demand' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#fetching-new-content-on-demand' aria-label='Anchor'></a>Fetching New Content on Demand</h2>
<p>This is where the Showcase application combines two techniques that may be a bit obscure.</p>
<p>The HTML form referenced a <code>person</code> stimulus controller. This controller has a number of responsibilities in the Showcase application. Here are the parts we’re interested in at the moment:</p>
<div class="highlight-wrapper group relative javascript">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-84m0kdth"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-84m0kdth"><span class="k">import</span> <span class="p">{</span> <span class="nx">Controller</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@hotwired/stimulus</span><span class="dl">"</span>
<span class="c1">// Connects to data-controller="person"</span>
<span class="k">export</span> <span class="k">default</span> <span class="kd">class</span> <span class="kd">extends</span> <span class="nx">Controller</span> <span class="p">{</span>
<span class="kd">static</span> <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">studio</span><span class="dl">'</span><span class="p">];</span>
<span class="nx">connect</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">id</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">token</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span>
<span class="dl">'</span><span class="s1">meta[name="csrf-token"]</span><span class="dl">'</span>
<span class="p">).</span><span class="nx">content</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">setType</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">fetch</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">data-url</span><span class="dl">'</span><span class="p">),</span> <span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">X-CSRF-Token</span><span class="dl">'</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">token</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span>
<span class="p">},</span>
<span class="na">credentials</span><span class="p">:</span> <span class="dl">'</span><span class="s1">same-origin</span><span class="dl">'</span><span class="p">,</span>
<span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span>
<span class="na">id</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span>
<span class="na">type</span><span class="p">:</span> <span class="nx">event</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">,</span>
<span class="na">studio_id</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">studioTarget</span><span class="p">.</span><span class="nx">value</span>
<span class="p">})</span>
<span class="p">}).</span><span class="nx">then</span> <span class="p">(</span><span class="nx">response</span> <span class="o">=></span> <span class="nx">response</span><span class="p">.</span><span class="nx">text</span><span class="p">())</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">html</span> <span class="o">=></span> <span class="nx">Turbo</span><span class="p">.</span><span class="nx">renderStreamMessage</span><span class="p">(</span><span class="nx">html</span><span class="p">));</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
</div>
</div>
<p>The <code>setType</code> method is invoked whenever the level selection changes. Looking again at part of the <code>form.select</code> input in <code>_form.html.erb</code>:</p>
<div class="highlight-wrapper group relative erb">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-rkqacqiw"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-rkqacqiw"><span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">select</span> <span class="ss">:type</span><span class="p">,</span> <span class="s1">'data-action'</span> <span class="o">=></span> <span class="s1">'person#setType'</span> <span class="cp">%></span>
</code></pre>
</div>
</div>
<p>The <code>setType</code> method itself uses the <a href='https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API' title=''>Fetch API</a>, passing in the body the <code>params</code> that will be made available to the controller. In this case, the <code>params</code> include the person’s <code>id</code>, <code>type</code>, and <code>studio.id</code>. The text of the response to this fetch request is extracted and then rendered as if it had been delivered via <a href='https://turbo.hotwired.dev/handbook/streams' title=''>TurboStream</a>.</p>
<p>There are two keys to making this work. Both are documented, albeit a bit obtusely.</p>
<p>First we need to deal with <code>X-CSRF-Token</code>. You will find it mentioned in the <a href='https://guides.rubyonrails.org/security.html' title=''>Securing Rails Applications</a>, as follows:</p>
<blockquote>
<p>By default, Rails includes an unobtrusive scripting adapter, which adds a header called X-CSRF-Token with the security token on every non-GET Ajax call. Without this header, non-GET Ajax requests won’t be accepted by Rails. When using another library to make Ajax calls, it is necessary to add the security token as a default header for Ajax calls in your library. To get the token, have a look at <code><meta name='csrf-token' content='THE-TOKEN'></code> tag printed by <code><%= csrf_meta_tags %></code> in your application view.</p>
</blockquote>
<p>The reference to an <em>unobtrusive scripting adapter</em> and <em>AJAX</em> are both anachronisms here, but the important essence here is that if you want non-GET HTTP requests to work you will need to extract the content from te <code>csrf-token</code> meta tag and place it in an <code>X-CSRF-Token</code> HTTP header.</p>
<p>To be fair, there are two alternatives to getting the CSRF token as was done here.</p>
<p>First, in some cases is possible to rewrite this particular request to use <code>encodeURIComponent</code> and HTTP GET, but in other cases (such as recording scores in the Showcase application), HTTP GET would not be appropriate. Passing JSON in request bodies is not only more convenient, but also can contain more complex nested data payloads than URL query strings.</p>
<p>Second, <a href='https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/requestSubmit' title=''>requestSubmit</a> can be used to submit an entire HTML form. HTML doesn’t support nested forms, but you can add additional, completely hidden, forms as needed; copy the relevant values to the hidden form’s input fields; and submit them.</p>
<p>If either of these alternatives work for you, use what you feel most comfortable with. Oftentimes it is useful to have a third alternative, especially when that alternative requires less code. Drag and drop is an clear example where HTTP GET is not appropriate, and where it generally is more straightforward to issue a fetch call than it is to construct and update a hidden form and submit it.</p>
<hr>
<p>The other important part is the method used to render the response. You will find this method mentioned in <a href='https://turbo.hotwired.dev/reference/streams#processing-stream-elements' title=''>Processing Stream Elements</a>:</p>
<blockquote>
<p>If you need to process stream actions from different source than something producing MessageEvents, you can use Turbo.renderStreamMessage(streamActionHTML) to do so.</p>
</blockquote>
<p>To have found this you would have needed to look at the documentation for Processing Stream Messages to find the description of how to handle <em>non</em>-Stream Messages.</p>
<p>While the documentation is a bit obscure, the resulting code is fairly straightforward: extract the body of the fetch response as HTML, and pass that HTML to <code>Turbo.renderStreamMessage</code>.</p>
<p>Now that the Studio and Type have been submitted to the server, the server has to render the Package input part of the form with the right stuff.</p>
<h2 id='rendering-new-content' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#rendering-new-content' aria-label='Anchor'></a>Rendering New Content</h2>
<p>Excerpt of the method in <code>people_controller.rb</code> that generates responses to <code>post_type</code> requests:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-tpp5yj32"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-tpp5yj32"> <span class="k">def</span> <span class="nf">post_type</span>
<span class="vi">@person</span> <span class="o">=</span> <span class="no">Person</span><span class="p">.</span><span class="nf">find_by_id</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span> <span class="o">||</span> <span class="no">Person</span><span class="p">.</span><span class="nf">new</span>
<span class="vi">@person</span><span class="p">.</span><span class="nf">studio</span> <span class="o">=</span> <span class="no">Studio</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:studio_id</span><span class="p">])</span>
<span class="vi">@person</span><span class="p">.</span><span class="nf">type</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:type</span><span class="p">]</span>
<span class="n">selections</span>
<span class="n">respond_to</span> <span class="k">do</span> <span class="o">|</span><span class="nb">format</span><span class="o">|</span>
<span class="nb">format</span><span class="p">.</span><span class="nf">turbo_stream</span> <span class="p">{</span>
<span class="n">render</span> <span class="ss">turbo_stream: </span><span class="n">turbo_stream</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="s1">'package-select'</span><span class="p">,</span>
<span class="n">render_to_string</span><span class="p">(</span><span class="ss">partial: </span><span class="s1">'package'</span><span class="p">))</span>
<span class="p">}</span>
<span class="nb">format</span><span class="p">.</span><span class="nf">html</span> <span class="p">{</span> <span class="n">redirect_to</span> <span class="n">people_url</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">selections</span>
<span class="p">.</span> <span class="nf">.</span> <span class="p">.</span>
<span class="nf">end</span>
</code></pre>
</div>
</div>
<p>This is straightforward. An <code>@person</code> object is constructed based on the <code>params</code> in the request. A private method is called to populate the <code>@packages</code> instance variable. Finally a <code>turbo_stream.replace</code> response is produced including the rendered package partial. This is the same partial that was originally rendered in the form.</p>
<figure class="post-cta">
<figcaption>
<h1>You can play with this right now.</h1>
<p>It’ll take less than 10 minutes to get your Rails application running globally.</p>
<a class="button bg-white text-violet-500 hover:bg-opacity-75 hover:text-violet-700 transition-colors" href="https://fly.io/docs/rails/">
Try Fly for free <span class='opacity-50'>→</span>
</a>
</figcaption>
<div class="image-container">
<img src="/static/images/cta-rabbit.webp" srcset="/static/images/cta-rabbit@2x.webp 2x" alt="">
</div>
</figure>
<h2 id='summary' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#summary' aria-label='Anchor'></a>Summary</h2>
<p>Overall, using <code>fetch</code> in response to DOM events, and processing the responses as if they were turbo stream messages, is a useful design pattern. Making it work required solving two problems: obtaining a CRSF token, and rendering a fetch response as a turbo stream messages.</p>
<p>On my development laptop, such requests can be processed quickly:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-unyqoq87"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-unyqoq87">Completed 200 OK in 5ms (Views: 0.0ms | ActiveRecord: 0.4ms | Allocations: 7213)
</code></pre>
</div>
</div>
<p>This means that if you can deploy your app close to your users so that the network latency is minimal, you can actually produce results comparable to single page applications in terms of form changes being perceived as instantaneous by users.</p>
<p>I hope you found this post useful and can find interesting ways to apply this technique in your Rails applications!</p>
/ruby-dispatch/turbostream-fetch/assets/ballroom-thumbnail.webpMaking Sense of Rails Assetshttps://fly.io/ruby-dispatch/making-sense-of-rails-assets/2022-08-22T00:00:00+00:002023-11-21T21:08:38+00:00<p><strong class='font-[550] text-navy-950'>The Rails asset ecosystem is at peak complexity as it transitions from Sprockets to Importmaps and Propshaft, by way of Webpacker. How does it affect people who build Rails apps? How should Rails plugin developers navigate the transition?</strong></p>
<p>Rails has always prided itself on convention over configuration and as a result, the community favors having one way to solve a problem vs. a bunch of different ways where a choice has to be made. That’s why the state of the asset pipeline is so surprising—there’s a lot of different ways to do it and a lot of different decisions that have to be made.</p>
<h2 id='today-in-rails-7' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#today-in-rails-7' aria-label='Anchor'></a>Today in Rails 7</h2>
<p>Here’s an over-simplified menu of your choices for an asset pipeline when creating a new Rails 7 application today:</p>
<h3 id='basic-asset-pipeline' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#basic-asset-pipeline' aria-label='Anchor'></a>Basic Asset Pipeline</h3>
<p>Running <code>rails new</code> as of Rails 7 will default to bundling the <code>importmap-rails</code> gem for JavaScript and the <code>sprockets</code> gem for CSS and image fingerprinting. If you’re building a Hotwire app or a simple Rails app that only requires a few JavaScript files, this approach is great since it doesn’t require npm, yarn, or any other parts of a typical JavaScript toolchain.</p>
<p>This approach does require HTTP/2 to be performant. Why? Because HTTP/2 allows websites to serve up separate files over a single connection.</p>
<p>Compare that to HTTP/1, which requires several connections to serve up multiple files. Creating connections to servers that are located in galaxies far far away has overhead, which is why Sprockets concatenated all JavaScript and CSS files into one big file. It reduced the number of connections that needed to be made to those distant servers.</p>
<p>There are some popular Rails web hosts out there that don’t support HTTP/2, so be sure to check with your host for support. Fly supports HTTP/2 out of the box, so no additional configuration is required to use import maps.</p>
<h3 id='complex-asset-pipeline' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#complex-asset-pipeline' aria-label='Anchor'></a>Complex Asset Pipeline</h3>
<p>If your project has a more complicated frontend that requires JavaScript or CSS compilation or manages dependencies with yarn or npm, you’ll want to use the <code>jsbundling-rails</code> and <code>cssbundling-rails</code> gems. Each of those gems brings in more complexity, which is only worth it if you absolutely need it.</p>
<h3 id='but-wait-theres-more' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#but-wait-theres-more' aria-label='Anchor'></a>But wait, there’s more!</h3>
<p>If you’re new to Rails, you should pick the Basic Asset Pipeline option and get back to shipping. Don’t get caught up in analysis paralysis on the perfect asset pipeline because it doesn’t exist.</p>
<p>But just for fun, and to prove a point about how confusing this can be, know that there’s a few other options for deploying Rails that work for more specialized needs:</p>
<ul>
<li><a href='https://github.com/rails/tailwindcss-rails' title=''><strong class='font-[550] text-navy-950'>Tailwind</strong></a> <strong class='font-[550] text-navy-950'>asset pipeline</strong> - The <code>tailwindcss-rails</code> gem wraps the <code>tailwindcss</code> CLI, and is a great option if you plan to use only Tailwind and know you won’t need to compile or manage other CSS dependencies.
</li><li><a href='https://github.com/rails/dartsass-rails' title=''><strong class='font-[550] text-navy-950'>Dart Sass</strong></a> <strong class='font-[550] text-navy-950'>asset pipeline</strong> - Like the Tailwind asset pipeline above, the <code>dartsass-rails</code> gem wraps the <code>dart-sass</code> binary and is great if you’re certain that’s all your project needs.
</li><li><strong class='font-[550] text-navy-950'>Sprockets-only asset pipeline</strong> - If you’re coming into Rails 7 with an application that’s using Sprockets to manage JavaScript assets, nothing says you have to change that. Sprockets will keep on chugging.
</li><li><strong class='font-[550] text-navy-950'>Webpacker asset pipeline</strong> - Rails 5.2 shipped with Webpacker, a wrapper for webpack, that made it possible for Rails apps to ship complex JS front-ends, like React, out of the box. While this gem will work in Rails 7, its been retired and is recommended to migrate to <a href='https://github.com/rails/jsbundling-rails/' title=''>jsbundling-rails</a>.
</li></ul>
<h2 id='pain-points' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#pain-points' aria-label='Anchor'></a>Pain points</h2>
<p>If you’re coming into Rails 7 from an older Rails app, there are few pain points that you can expect to run into:</p>
<h3 id='migrating-pipelines' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#migrating-pipelines' aria-label='Anchor'></a>Migrating pipelines</h3>
<p>The documentation and tools for migrating pipelines isn’t fully baked yet, though efforts are being made to make this clearer, as evident in a <a href='https://discuss.rubyonrails.org/t/guide-to-rails-7-and-the-asset-pipeline/80851' title=''>Rails forum post</a> and <a href='https://github.com/rails/rails/pull/45400' title=''>Draft Github PR that updates the Asset Pipeline Guide</a>. Improving documentation will help eliminate some of the confusion around the asset pipeline.</p>
<p>Another thing to think about: if you create a new Rails 7 app with the basic pipeline, then find out in the future that you need to switch to <code>jsbundling-rails</code>, you’ll have to manually move around your asset files and reconfigure a few things.</p>
<h3 id='running-the-development-server' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#running-the-development-server' aria-label='Anchor'></a>Running the development server</h3>
<p>For a long time, the way to run a Rails development server was <code>bin/rails server</code> . Some asset pipeline configurations require running development servers in a separate process from the Rails development server. There is a slight learning curve for Rails developers who have to switch from running <code>bin/rails server</code> to <code>bin/dev</code> and learn about <code>Procfile</code>.</p>
<h3 id='difficulty-using-assets-from-rails-plugins' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#difficulty-using-assets-from-rails-plugins' aria-label='Anchor'></a>Difficulty using assets from Rails plugins</h3>
<p>Where you’re probably running into issues today is how to get assets from Rails plugins, like <code>local_time</code>, working in Rails. Why is that? Let’s take a closer look starting with the Installation section of the project’s README file:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-ac8vfpbk"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-ac8vfpbk">## Installation
1. Add `gem 'local_time'` to your Gemfile.
2. Include `local-time.js` in your application's JavaScript bundle.
Using the asset pipeline:
```js
//= require local-time
```
Using the [local-time npm package](https://www.npmjs.com/package/local-time):
```js
import LocalTime from "local-time"
LocalTime.start()
```
</code></pre>
</div>
</div>
<p>The first direction for inclusion in the asset bundle is Sprockets syntax. Since Sprockets managed load paths, it knew that <code>//= require local-time</code> should look in that project’s <code>lib/assets/javascripts/src</code> path to find the asset.</p>
<p>The second direction for inclusion makes life a little more difficult for Rails plugin developers. In addition to maintaining the Ruby gem, they also now have to maintain an npm package and version that separately from the gem.</p>
<p>The third direction is missing, which would instruct Rails 7 application developers on how to pin the local-time npm package via <code>bin/importmap pin</code>. For the Rails plugin developer, there’s not much direction on if they should distribute their JavaScript assets with the Rails gem or separately as an npm package.</p>
<p>This confusion exists today because of the different asset management systems that Rails currently has in play: sprockets, webpacker, jsbundling-rails, and importmaps. There’s no One Way™ for third party gem developers to integrate their assets with Rails.</p>
<p>Fortunately work is being done by the Rails community to solve a few of these pain points.</p>
<h2 id='the-future' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-future' aria-label='Anchor'></a>The Future</h2>
<p>In the future, <a href='https://github.com/rails/propshaft' title=''>Propshaft</a> will serve as the interface between Rails applications and assets. It will replace all that Sprockets does and focus on these four things:</p>
<ol>
<li><strong class='font-[550] text-navy-950'>Load paths</strong> - Propshaft will keep track of asset load paths from the app and Rails gem plugins. This should make installing Rails plugin gems much easier since they’ll register with Propshaft, eliminating the guesswork needed to find assets.
</li><li><strong class='font-[550] text-navy-950'>Digest stamping -</strong> Assets defined in the load paths will be digested and copied into the <code>./public</code> directory, along with a <code>manifest.json</code> file, so Rails can serve up assets with long cache expiries. This is a well known technique that makes web applications load faster.
</li><li><strong class='font-[550] text-navy-950'>Development server</strong> - Propshaft <a href='https://github.com/rails/propshaft/blob/main/lib/propshaft/railtie.rb#L36-L42' title=''>will run a development server inside of Rails</a>, which means it will be possible to run some development environments via <code>bin/rails server</code> without a Procfile. This is still under active development, so expect changes in the final implementation.
</li><li><strong class='font-[550] text-navy-950'>Basic compilers</strong> - Propshaft will support basic compilation steps, like replacing <code>url(asset)</code> strings in CSS files with <code>url(digested-asset)</code>. Complex compilations will be delegated out to tools better suited for that purpose.
</li></ol>
<p>Rails will continue recommending importmaps for JavaScript assets when running <code>rails new</code>, but instead of including Sprockets, it will include Propshaft. Sprockets will probably continue being supported for a while, but it will be recommended to switch to Propshaft as the way to manage Rails assets.</p>
<hr>
<h2 id='the-past' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-past' aria-label='Anchor'></a>The Past</h2>
<p>You might be thinking, “how did the Rails asset pipeline get to be such a beast?”. Like most questions that end with, “get to be such a beast”, it’s a long story.</p>
<p>Rails has been around for over a decade, a time long ago before HTTP/2 existed and the phrase “Backend JavaScript developer” was an oxymoron. Come on a journey with me and explore its past.</p>
<h3 id='the-time-before-asset-management' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-time-before-asset-management' aria-label='Anchor'></a>The Time Before Asset Management</h3>
<p>When Rails first started, HTTP/2 didn’t exist, JavaScript development as a full-time job was a foreign concept, and asset management was a matter of throwing <a href='http://script.aculo.us' title=''>Scriptaculus</a>, <a href='http://prototypejs.org' title=''>Prototype</a>, and an <code>application.js</code> file into the <code>./public/assets</code> directory. No fingerprinting was necessary because there were so few moving parts.</p>
<p>There were third-party gems, like <a href='https://rubygems.org/gems/jammit' title=''>jammit</a>, that were available for production websites that cared about asset efficiency, but for the most part it’s something Rails devs didn’t think about.</p>
<p>Life was good, then JavaScript file sizes starting getting bigger and became a much bigger concern of web application development.</p>
<h3 id='hello-my-name-is-sprockets' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#hello-my-name-is-sprockets' aria-label='Anchor'></a>Hello, my name is Sprockets</h3>
<p>A few years later, when we swore off Prototype because it extended core JavaScript prototypes and moved on to jQuery, Sprockets came onto the scene. Most of us remember Sprockets as a huge pain, but it was actually pretty good when it arrived into Rails because source maps, transpiling, and all the other fancy stuff that JS compilers do wasn’t that pervasive.</p>
<h3 id='move-over-sprockets-webpacker-is-in-town' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#move-over-sprockets-webpacker-is-in-town' aria-label='Anchor'></a>Move over Sprockets, Webpacker is in town</h3>
<p>At some point the JavaScript community flew past Sprockets in terms of pipeline sophistication. Rails developers envied source maps, tree shaking, and all sorts of other goodies that made for more efficient asset delivery. Sprockets couldn’t keep up, so Webpacker was born.</p>
<p>Remember, this was all before Turbo landed. The way to build a high budget compelling Rails app at the time was to deploy a front-end Framework like React, Vue, or Backbone.js.</p>
<h3 id='hmm-maybe-rails-apps-shouldnt-run-so-much-javascript-and-http-2-solves-some-asset-pipeline-problems' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#hmm-maybe-rails-apps-shouldnt-run-so-much-javascript-and-http-2-solves-some-asset-pipeline-problems' aria-label='Anchor'></a>Hmm, Maybe Rails Apps Shouldn’t Run so Much JavaScript and HTTP/2 Solves Some Asset Pipeline Problems</h3>
<p>At some point a few folks in the Rails community had the revelation that fast, responsive web applications that developers typically associate with JavaScript-rich applications could be built without all the complexity of a single-page JavaScript application that talks to its server via an API. Turbo was born!</p>
<p>Turbo has nothing to do with the asset pipeline, so how is it relevant? Most Turbo apps require a few JavaScript files that don’t need any fancy compiler technology, so it doesn’t need a complex JavaScript compilation process. Turbo accomplishes this feat by keeping application state on the server, rendering HTML fragments, and delivering it over the wire to the browser where it swaps out parts of the DOM.</p>
<p>At the same time it was realized that HTTP/2 could deliver a lot of small asset files without incurring the performance penalties imposed by HTTP/1, so importmaps was born.</p>
<p>These two ideas came together in Rails 7 and gave us a much simpler way to build web applications without the complexity associated with front-end heavy JavaScript applications.</p>
/ruby-dispatch/making-sense-of-rails-assets/assets/making-sense-of-rails-assets-thumbnail.webpSemi-Static Websiteshttps://fly.io/ruby-dispatch/semi-static-websites/2022-08-17T00:00:00+00:002023-11-21T21:08:38+00:00<div class="lead"><p>Fly makes it easy to deploy server-side rendered (SSR) content sites to a global fleet of servers close to your readers <em>without</em> a content distribution network (CDN). We’ll look at how to accomplish that with <a href="https://sitepress.cc/" title="">Sitepress</a>, a Ruby site generator that can ran as a stand-alone SSR, static website, or be embedded inside of a Rails app.</p>
</div>
<p>Static websites have exploded in popularity over the past few years. What is it that people like so much about static site generators?</p>
<ul>
<li><strong class='font-[550] text-navy-950'>Low operational complexity</strong> - Static websites can be deployed to a production environment without the need for a database, caching server, or other service dependencies. All that’s needed is a pile of HTML, CSS, and JavaScript files and a fast server, like nginx.
</li><li><strong class='font-[550] text-navy-950'>Things break <em>before</em> they’re deployed</strong> - If there’s an error, its usually caught during the compilation phase by the site compiler. The only exception to this are static sites that include tons of JavaScript.
</li><li><strong class='font-[550] text-navy-950'>Pretty darn fast</strong> - Static websites that keep an eye on asset sizes are generally pretty fast around the world when deployed behind a CDN, but only if those caches are warmed up. More on that later.
</li><li><strong class='font-[550] text-navy-950'>Content is files</strong> - Site generators typically manage content as a bunch of text, image, JavaScript, and CSS files in folders. This means they can be stored on a file system and tracked with amazing tools like git, which bills itself as <code>the stupid content tracker</code>. Dynamic systems, like Wordpress, use databases to store content, which requires versioning schemes that are implemented at the application level.
</li></ul>
<p>Despite these dreamy characteristics of running static websites in production, it does come with a few trade-offs, especially if there’s a few dynamic things that are needed, like publishing content with a future publish date.</p>
<p>Let’s look at a few approaches for how one might accomplish such a feat so we can better understand these trade-offs.</p>
<h2 id='problem-how-do-i-publish-future-content' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#problem-how-do-i-publish-future-content' aria-label='Anchor'></a>Problem: How Do I Publish Future Content?</h2>
<p>A common use case when dealing with content, like a blog, is scheduling a post to be published in the future. It’s a simple task for a dynamic content management system, but for static site generators, it requires some work. Here’s a few different approaches to the problem.</p>
<h3 id='the-static-way-use-cron-to-build-and-publish-the-site' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-static-way-use-cron-to-build-and-publish-the-site' aria-label='Anchor'></a>The Static Way: Use <code>cron</code> to Build and Publish The Site</h3>
<p>One way to publish future content with a static website is to schedule an hourly or daily task in <code>cron</code> that builds all the HTML pages in the website and uploads them to a production environment. This works fine for a small website that’s not updated frequently, but it falls over for larger websites that need to publish content at more fine-grained intervals. Imagine wiring up a cronjob that runs every minute to compile and upload a website with a few thousand pages?</p>
<h3 id='the-database-way-run-a-cms-like-refinery-or-wordpress' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-database-way-run-a-cms-like-refinery-or-wordpress' aria-label='Anchor'></a>The Database Way: Run a CMS like Refinery or Wordpress</h3>
<p>On the other end of the spectrum there’s the Content Management System. The most popular CMS in the world is WordPress. For Rails, the most popular CMS is Refinery <a href='https://www.ruby-toolbox.com/categories/content_management_systems' title=''>according to the Ruby Toolbox</a>.</p>
<p>This approach to managing content comes with a lot of complexity. For starters, these approaches require databases. Databases are hard to sync between production, staging, and development environments. Databases break. It’s difficult to version and merge conflicting data in the database if people are editing the same content. Usually some sort of caching layer needs to be built above the content generated by the database. It’s a lot of extra complexity that might not be worth it.</p>
<h3 id='the-middle-road-deploy-a-semi-static-website-to-fly' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-middle-road-deploy-a-semi-static-website-to-fly' aria-label='Anchor'></a>The Middle Road: Deploy a Semi-Static Website to Fly</h3>
<p>Semi-static websites, also known as server-side rendered (SSR) sites, run a small server in production that renders content per request. For a blog, that means the server could check a <code>publish_at</code> frontmatter key on a markdown file like this:</p>
<div class="highlight-wrapper group relative md">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-m1pzvr2u"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-m1pzvr2u"><span class="nn">---</span>
<span class="na">title</span><span class="pi">:</span> <span class="s">My First Blog Post</span>
<span class="na">publish_at</span><span class="pi">:</span> <span class="s">January 1, </span><span class="m">2042</span>
<span class="nn">---</span>
Hello! I hope 2042 is a great year.
</code></pre>
</div>
</div>
<p>And quickly figure out whether or not to display the post based on the server’s current time.</p>
<p>We’re going to use a Ruby site generator called <a href='https://sitepress.cc/' title=''>Sitepress</a> to deploy a semi-static website to Fly’s global infrastructure. Why not Bridgetown, Jekyll, and Middleman? Those are all really great site generators, but they’re focused on generating static HTML, CSS, and JavaScript assets, which won’t work that great for server-side rendered sites. Outside of Ruby there’s tons of site generators, but this is written for Ruby Dispatch where we talk about all things Ruby.</p>
<h2 id='how-to-build-and-deploy-a-semi-static-website-to-fly' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#how-to-build-and-deploy-a-semi-static-website-to-fly' aria-label='Anchor'></a>How to build and deploy a semi-static website to Fly</h2>
<p>It’s pretty quick! Here’s how to do it:</p>
<ol>
<li>Clone the repo <a href='https://github.com/sitepress/standalone-starter' title=''>https://github.com/sitepress/standalone-starter</a>.
</li></ol>
<div class="highlight-wrapper group relative cmd">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-52m6hnqi"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight cmd'><code id="code-52m6hnqi">git clone git@github.com:sitepress/standalone-starter.git
</code></pre>
</div>
</div>
<ol>
<li><a href='https://fly.io/docs/getting-started/installing-flyctl/' title=''>Install the Fly CLI</a> and signup for an account.
</li><li>Now we’re going to run a command that provisions the app and deploys it:
</li></ol>
<div class="highlight-wrapper group relative cmd">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-9laugkoz"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight cmd'><code id="code-9laugkoz">fly launch --copy-config --dockerfile Dockerfile --now
</code></pre>
</div>
</div>
<ol>
<li>Once that finishes, run <code>fly open</code> and you should see a website that looks like this:
</li></ol>
<p><img src="https://slabstatic.com/prod/uploads/p1b436gf/posts/images/v5scGQhm4E_evdFPW480l0YC.webp" /></p>
<p>Hooray! You’ve published your first semi-static website. It’s running a webrick server that renders everything per-request.</p>
<h3 id='how-fast-is-the-website-we-just-deployed' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#how-fast-is-the-website-we-just-deployed' aria-label='Anchor'></a>How fast is the website we just deployed?</h3>
<p>Now let’s see what things look like for people on the other side of the world by running <code>fly curl</code>, a nifty little tool that loads your website from various Fly outposts.</p>
<div class="highlight-wrapper group relative cmd">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-ufketsse"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight cmd'><code id="code-ufketsse">fly curl https://$YOUR_SITE_NAME.fly.dev
</code></pre>
</div>
</div><div class="highlight-wrapper group relative output">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-l74yi5bg"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight output'><code id="code-l74yi5bg">REGION STATUS DNS CONNECT TLS TTFB TOTAL
ams 200 0.7ms 0.8ms 29ms 193.5ms 195.1ms
cdg 200 0.7ms 0.9ms 29.1ms 179.5ms 181.1ms
dfw 200 0.7ms 0.9ms 29.5ms 71.3ms 73.5ms
ewr 200 0.5ms 0.7ms 25.1ms 90.6ms 91.8ms
fra 200 0.6ms 0.8ms 30.5ms 204.7ms 205.9ms
hkg 200 0.6ms 0.7ms 21.7ms 186.9ms 188.5ms
lax 200 0.5ms 0.7ms 21.9ms 50.5ms 51.2ms
lhr 200 0.8ms 1ms 29.2ms 174.3ms 175.9ms
mia 200 0.8ms 1.1ms 33ms 116.4ms 160.6ms
nrt 200 0.4ms 0.5ms 17.4ms 135.8ms 136.5ms
ord 200 0.6ms 0.8ms 22.8ms 93.3ms 95.2ms
scl 200 1.1ms 1.5ms 48.3ms 211.3ms 213.3ms
sea 200 0.4ms 0.5ms 16.3ms 57.4ms 57.9ms
sin 200 0.5ms 0.6ms 17.5ms 204.6ms 205.2ms
sjc 200 3.3ms 3.4ms 34.9ms 46.1ms 46.4ms
syd 200 0.7ms 0.9ms 24.9ms 192.9ms 193.6ms
yyz 200 0.6ms 0.8ms 31.6ms 99ms 99.9ms
</code></pre>
</div>
</div>
<p>The time-to-first-byte (TTFB) times on the other side of the world probably aren’t all that great, which means people trying to read your website are sitting there waiting. TTFB is the amount of time people have to wait after typing <code>your website.com</code> into their browser and receiving the first bytes of HTML.</p>
<h3 id='provision-and-deploy-to-servers-around-the-world' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#provision-and-deploy-to-servers-around-the-world' aria-label='Anchor'></a>Provision and deploy to servers around the world</h3>
<p>Let’s fix that problem by deploying this website closer to them, without a CDN, by telling Fly we’re cool running our site in these regions:</p>
<div class="highlight-wrapper group relative cmd">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-jzrogb50"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight cmd'><code id="code-jzrogb50">fly regions set sin fra ord
</code></pre>
</div>
</div><div class="highlight-wrapper group relative output">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-eun7qlqg"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight output'><code id="code-eun7qlqg">Region Pool:
fra
ord
sin
Backup Region:
</code></pre>
</div>
</div>
<p>This command tells Fly the parts of the world you want to deploy the website, but the servers aren’t running there yet. Let’s spin them up.</p>
<div class="highlight-wrapper group relative cmd">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-eo94nfk1"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight cmd'><code id="code-eo94nfk1">fly scale count 3 --max-per-region=1
</code></pre>
</div>
</div>
<p>Fly will scale up your semi-static site in each of these regions so they’re ready to respond to requests as they come in.</p>
<p>Let’s see how those regions scaled up.</p>
<div class="highlight-wrapper group relative cmd">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-u6eqgsv0"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight cmd'><code id="code-u6eqgsv0">fly status
</code></pre>
</div>
</div><div class="highlight-wrapper group relative output">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-y93pg4c4"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight output'><code id="code-y93pg4c4">App
Name = $YOUR_SITE_NAME
Owner = personal
Version = 2
Status = running
Hostname = $YOUR_SITE_NAME.fly.dev
Platform = nomad
Instances
ID PROCESS VERSION REGION DESIRED STATUS HEALTH CHECKS RESTARTS CREATED
394677de app 2 ord run running 1 total, 1 passing 0 1m6s ago
4de70730 app 2 sin run running 1 total, 1 passing 0 1m6s ago
8f6646aa app 2 fra run running 1 total, 1 passing 0 2m21s ago
</code></pre>
</div>
</div><h3 id='now-how-fast-is-our-website' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#now-how-fast-is-our-website' aria-label='Anchor'></a>Now how fast is our website?</h3>
<p>Let’s run <code>fly curl</code> again and see what happened to the latency:</p>
<div class="highlight-wrapper group relative cmd">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-zjompq7a"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight cmd'><code id="code-zjompq7a">fly curl https://$YOUR_SITE_NAME.fly.dev
</code></pre>
</div>
</div><div class="highlight-wrapper group relative output">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-eteo8tv3"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight output'><code id="code-eteo8tv3">REGION STATUS DNS CONNECT TLS TTFB TOTAL
ams 200 0.7ms 1ms 29ms 37.7ms 39.4ms
cdg 200 0.9ms 1.1ms 22.9ms 44.5ms 45.9ms
dfw 200 0.8ms 1.2ms 25.9ms 68.1ms 69.3ms
ewr 200 6.8ms 7.1ms 85.5ms 70.3ms 71.3ms
fra 200 0.8ms 1.1ms 26.9ms 33ms 34.4ms
hkg 200 0.9ms 1.1ms 22.7ms 70ms 71.7ms
lax 200 0.5ms 0.7ms 21.7ms 86.4ms 87.1ms
lhr 200 0.8ms 1.1ms 24.5ms 36.4ms 38ms
mia 200 0.8ms 1.2ms 25.3ms 69ms 70.8ms
nrt 200 0.4ms 0.6ms 18.1ms 94.1ms 94.6ms
ord 200 0.4ms 0.6ms 21.8ms 51.9ms 53ms
scl 200 1.1ms 1.4ms 39.8ms 168.1ms 170.3ms
sea 200 0.4ms 0.5ms 16.1ms 83.2ms 91.6ms
sin 200 0.4ms 0.5ms 17.4ms 26.3ms 26.9ms
sjc 200 0.4ms 0.5ms 28.1ms 83.6ms 83.7ms
syd 200 0.9ms 1.1ms 23.4ms 113.7ms 115.1ms
yyz 200 1ms 1.5ms 41.8ms 60.8ms 62.3ms
</code></pre>
</div>
</div>
<p>Much better! The TTFB time decreased, which means people who are reading the website see the content instantly (under 250ms) as far as they’re concerned.</p>
<h2 id='fly-content-distribution-networks-your-customers-and-you' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#fly-content-distribution-networks-your-customers-and-you' aria-label='Anchor'></a>Fly, Content Distribution Networks, Your Customers, and You</h2>
<p>With Fly, it is possible to cut out the middleman CDN and simply run content servers closer to your customers. CDNs are great, but they do add latency to your application for a few reasons:</p>
<ol>
<li>A CDN with a cold cache has to fetch the content from the origin. This adds latency to the initial request.
</li><li>When the cache from a CDN expires, it has to check the origin for a fresh resource which again, adds latency to the request. Some CDNs can be configured to serve up the stale content while requesting the new stuff from the origin, but nobody wants something that’s stale. If people like stale stuff bakeries wouldn’t mark down day-old-bread.
</li></ol>
<p>For static websites, this isn’t a huge deal, but for mixed websites where all application requests go through the CDN and the cache times are low like a blog or news website, its at least an extra hop that simply isn’t necessary when running servers close to your users.</p>
<h2 id='theres-lots-of-reasons-to-deploy-semi-static-websites' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#theres-lots-of-reasons-to-deploy-semi-static-websites' aria-label='Anchor'></a>There’s lots of reasons to deploy semi-static websites</h2>
<p>Here’s a few that you could try for your own projects:</p>
<ul>
<li><strong class='font-[550] text-navy-950'>Always up-to-date project README files</strong> - Lots of open source projects have a website that repeat content from the <code>README.md</code> file at the root of their project. With a semi-static site, its easy to fire off an HTTP request for an open-source projects latest <code>README.md</code> file and render it within the project website. The starter app <a href='https://github.com/sitepress/standalone-starter/blob/main/pages/projects/sitepress.html.md' title=''>shows how a Github README could be rendered on a project page</a>.
</li><li><strong class='font-[550] text-navy-950'>Localization</strong> - A semi-static site could localize content depending on the <code>FLY_REGION</code> or users IP address.
</li><li><strong class='font-[550] text-navy-950'>Treat Your Content As Data</strong> - Imagine a world where you could get a list of relevant help articles to your customers from within your application? It’s possible with Sitepress via a line of code that looks like <code>HelpPages.tags(:login, :security, :account_management)</code>. Treating content as data opens up a lot of interesting use cases for tightly integrating content with your app, which can make for a great experience for your customers.
</li><li><strong class='font-[550] text-navy-950'>Community Websites</strong> - Git is the ultimate content management system for communities, especially when it’s backed with workflows like “Open a PR to edit content”. It’s how <a href='https://github.com/superfly/docs' title=''>Fly’s docs are managed</a>. Be sure to include an “Edit with Github” link on the page that opens the content up in Github’s edit view so people can make contributions for quick edits, like typos.
</li><li><strong class='font-[550] text-navy-950'>Run it in your Rails Apps</strong> - If you’re a small team or solo developer, Sitepress can be <a href='https://sitepress.cc/getting-started/rails/' title=''>embedded in your Rails apps</a> and integrate directly with your routes files. If the Rails application has a database, you’ll want to read about <a href='https://fly.io/blog/run-ordinary-rails-apps-globally/' title=''>how to run ordinary rails apps globally</a>.
</li></ul>
/ruby-dispatch/semi-static-websites/assets/semi-static-websites-thumbnail.webpRun Ordinary Rails Apps Globallyhttps://fly.io/ruby-dispatch/run-ordinary-rails-apps-globally/2021-08-10T00:00:00+00:002023-11-21T21:08:38+00:00<div class="lead"><p>Postgres on Fly.io is now free for small projects. This post is about making Rails fast, though. If you just want to get your app launched, <a href="https://fly.io/docs/speedrun/" title="">try us out first</a>. You can be up and running in just a few minutes.</p>
</div>
<p>If you’ve used your own Rails application from another continent, you may get the feeling that physics has beaten your performance tuning efforts. Page loads feel a bit sluggish, even with all the right database indexes and fancy CDN-backed assets.</p>
<p><a href='https://fly.io/blog/last-mile-redis/' title=''>We’ve said it before</a>: when it comes to responsiveness, sub-100ms times are the magic number; below 100ms, and things <em>feel</em> instantaneous.</p>
<p>Now, simple regional asset caching — the CDN pitch — can bring apps closer to 100ms response times. But what if you could easily deploy your <em>application</em> globally? Not just images, but application logic. And what if you could do it without changes to your code?</p>
<p>This type of global deployment sounds like a major infrastructure project — unrealistic to undertake in the short term, and long-term reserved for giant companies with serious technical faangs. It shouldn’t be that way. All apps should run close to end users. And with the right plumbing, we can distribute and scale globally from day one.</p>
<p>Fly.io has been doing a lot of cool stuff with <a href='https://fly.io/blog/building-a-distributed-turn-based-game-system-in-elixir/' title=''>Elixir and Phoenix</a>. Elixir, built on Erlang’s distributed-by-design BEAM runtime, begs to be clustered and geographically distributed, and it really sings on Fly.io. But people do real work in Rails, too. That’s why I wrote the <a href='https://github.com/superfly/fly-ruby' title=''>fly-ruby gem</a>. It’s a tiny library that makes it trivial to deploy Rails apps on a global platform like Fly.io. No new framework or functional language learning required.</p>
<p>This post is going to talk you through how <a href='https://github.com/superfly/fly-ruby' title=''>fly-ruby</a> works. Before we dig into the details of the gem itself, it’s worth a minute to talk about how Fly.io works and what it means to optimize an application for it.</p>
<h2 id='what-fly-io-does' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#what-fly-io-does' aria-label='Anchor'></a>What Fly.io Does</h2>
<p>For our 100ms performance goal, Fly.io has two major features that Rails can take advantage of.</p>
<p><strong class='font-[550] text-navy-950'>Region-local database replicas:</strong> <a href='https://fly.io/docs/getting-started/multi-region-databases/' title=''>Deploying a global Postgres cluster of read replicas on Fly.io</a> is easy. And read replicas are, by themselves, a good first step to improving Rails performance.</p>
<p>Rails instances read from their corresponding regional replica, so your “find local recipes” app serves information about Pan-fried rice noodles in Hong Kong from a Postgres replica in Hong Kong, and Italian beef sandwiches from a replica in Chicago.</p>
<p><strong class='font-[550] text-navy-950'>Replayable HTTP requests:</strong> Read replicas work great for <em>retrieving</em> data locally. But we sometimes need to write to the database, and replicas don’t handle writes.</p>
<p>Somewhere in the world (let’s say Paris) we’ll have a main Postgres — that means our write requests need to make their way to Paris, over Fly.io’s private network.</p>
<p>For example, an HTTP request may arrive in Hong Kong that needs to write to the main database in Paris. We can tell Fly.io’s proxy - via the <code>Fly-Replay</code> response header - to <em>move the entire HTTP request to Paris’s Rails instance</em>, and it will just work.</p>
<h2 id='the-magical-fly-io-ruby-gem' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-magical-fly-io-ruby-gem' aria-label='Anchor'></a>The magical Fly.io Ruby Gem</h2>
<p>So let’s do some testing and figure out how to get below that 100ms threshold from Paris, Chicago, Sydney and Santiago, Chile.</p>
<p>We’ve tried most performance testing tools and our current favorite is <a href='https://k6.io' title=''>k6</a>, a modern, open source web performance testing tool. It’s unique in its approach: you write tests in Javascript, interpreted and executed in a Go runtime. It has exquisite documentation - especially for those unfamiliar with web performance testing. Their hosted option supports distributed tests, but we can also <a href='https://github.com/jsierles/fly-k6' title=''>run tests from a global Fly.io app</a>!</p>
<p>First, we should see how a vanilla, single-region deployment fares. We just need <a href='https://fly.io/docs/getting-started/multi-region-databases' title=''>a Postgres database</a> in Paris, and a Rails app deployed in the same region. It would be weird to write a whole article without mentioning food, so here’s a little <a href='https://cookherenow.com' title=''>recipe search app</a> that’s good for testing. For bonus points, it shows different recipes to people in different cities.</p>
<p>Here’s how it performs:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-v5xm1g5a"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-v5xm1g5a">Single region deployment: Time to First Byte
cdg 72.7ms
ord 168.7ms
syd 286.2ms
scl 442.9ms
</code></pre>
</div>
</div>
<p>With the current, single region app config, every request is bounced to Paris. Great for people in Paris, not great for people in Santiago with a hankerin’ for Pastel de Choclo.</p>
<p>If we deploy our app to more regions, we get a nasty surprise:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-vso77152"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-vso77152">Multiregion deployment with single database: Time to First Byte
cdg 76ms
ord 258ms
syd 528.7ms
scl 1341ms
</code></pre>
</div>
</div>
<p>Performance got worse!? This isn’t a very good sales pitch. There’s a simple explanation, though, and we’re halfway to faster Chilean recipe suggestions.</p>
<p>The Rails instance in Sydney still needs to query the database — often multiple times — and <em>each</em> of those database queries bounces around the world to Paris (over an encrypted private network). Adding latency between the app server and database multiplies internet latency. One round trip from Sydney to Paris might take 400ms. Ten in a row feels like an hour.</p>
<p>Now, here’s the sales pitch. The <code>fly-ruby</code> gem will switch to regional replicas for database reads and <em>magically route write request to the primary database</em>.</p>
<p>If we add the <code>fly-ruby</code> gem, set the PRIMARY_REGION environment variable, here’s what happens:</p>
<div class="highlight-wrapper group relative ">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-h6kt313x"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-h6kt313x">Multiregion deployment with regional database replicas: Time to First Byte
cdg 75.1ms
ord 50.4ms
syd 45.8ms
scl 84.4ms
</code></pre>
</div>
</div>
<p>One tiny configuration change, 90% latency reduction, and our Rails app suddenly responds in sub-100ms. No architecture work required.</p>
<h2 id='its-not-actually-magic' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#its-not-actually-magic' aria-label='Anchor'></a>It’s not actually magic</h2>
<p>This 300-line gem doesn’t really do much. Postgres and the Fly.io global proxy <a href='https://fly.io/blog/globally-distributed-postgres/' title=''>do all the heavy lifting</a>. It’s a set of Rack middleware that does the last little bit of work for you. And it’s usable in any Rack-compatible application, for people who like their Ruby without restrictive rails.</p>
<p>The magic here lives in the <code>Fly-Replay</code> header. I pass a <strong class='font-[550] text-navy-950'>state</strong><em>:</em> an arbitrary value written to the <code>Fly-Replay-Src</code> header, appended to the final replayed request to the primary application instance.</p>
<p>This state assists the middleware in handling the replay under different conditions, as we’ll see below.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-qhbz0ty0"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-qhbz0ty0"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">replay_in_primary_region!</span><span class="p">(</span><span class="n">state</span><span class="p">:)</span>
<span class="n">res</span> <span class="o">=</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Response</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
<span class="s2">""</span><span class="p">,</span>
<span class="mi">409</span><span class="p">,</span>
<span class="p">{</span><span class="s2">"Fly-Replay"</span> <span class="o">=></span> <span class="s2">"region=</span><span class="si">#{</span><span class="no">Fly</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">primary_region</span><span class="si">}</span><span class="s2">;state=</span><span class="si">#{</span><span class="n">state</span><span class="si">}</span><span class="s2">"</span><span class="p">}</span>
<span class="p">)</span>
<span class="n">res</span><span class="p">.</span><span class="nf">finish</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>I exploit the web perf rule of thumb that <strong class='font-[550] text-navy-950'>most requests are reads, and most reads use HTTP GET requests.</strong> I can safely reconnect Rails to the region-local database replica. The gem builds the replica URI using Fly.io’s DNS service discovery and the <code>FLY_REGION</code> environment variable.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-jbi1yngj"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-jbi1yngj"><span class="n">database_uri</span> <span class="o">=</span> <span class="no">URI</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="no">ENV</span><span class="p">[</span><span class="s1">'DATABASE_URL'</span><span class="p">])</span>
<span class="n">database_uri</span><span class="p">.</span><span class="nf">host</span> <span class="o">=</span> <span class="s2">"</span><span class="si">#{</span><span class="no">ENV</span><span class="p">[</span><span class="s1">'FLY_REGION'</span><span class="p">]</span><span class="si">}</span><span class="s2">.</span><span class="si">#{</span><span class="n">database_uri</span><span class="p">.</span><span class="nf">hostname</span><span class="si">}</span><span class="s2">"</span>
<span class="n">database_uri</span><span class="p">.</span><span class="nf">port</span> <span class="o">=</span> <span class="mi">5433</span>
<span class="n">database_uri</span><span class="p">.</span><span class="nf">to_s</span>
</code></pre>
</div>
</div>
<p>As a result, HTTP GET requests are passed directly down to the Rails application. And, in the normal case, they return after a speedy round trip to the database replica.</p>
<p>But GET requests occasionally perform writes. It’s dirty, but true.</p>
<p>Fortunately, Postgres won’t allow writes to a Postgres read replica. When a database write slips through, the Ruby Postgres library throws an exception. The gem inserts another middleware — at the <em>bottom</em> of the stack — to catch the<code>PG::ReadOnlySqlTransaction</code> exception. This halts the response and asks Fly.io to replay the original request in the primary region.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-753b0a3y"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-753b0a3y"><span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
<span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
<span class="k">rescue</span> <span class="no">PG</span><span class="o">::</span><span class="no">ReadOnlySqlTransaction</span><span class="p">,</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">StatementInvalid</span> <span class="o">=></span> <span class="n">e</span>
<span class="k">if</span> <span class="n">e</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">PG</span><span class="o">::</span><span class="no">ReadOnlySqlTransaction</span><span class="p">)</span> <span class="o">||</span> <span class="n">e</span><span class="o">&</span><span class="p">.</span><span class="nf">cause</span><span class="o">&</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">PG</span><span class="o">::</span><span class="no">ReadOnlySqlTransaction</span><span class="p">)</span>
<span class="no">RegionalDatabase</span><span class="p">.</span><span class="nf">replay_in_primary_region!</span><span class="p">(</span><span class="ss">state: </span><span class="s2">"captured_write"</span><span class="p">)</span>
<span class="k">else</span>
<span class="k">raise</span> <span class="n">e</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>It could stop here. But there are a bunch of requests for which we don’t have to do this dance. It’s safe to assume that <strong class='font-[550] text-navy-950'>non-idempotent HTTP requests intend to write to the database.</strong> This includes, by default, POST, PUT, PATCH and DELETE requests.</p>
<p>So, from high in the middleware stack, the gem halts and replay probably-write requests in the primary region, which prevents unnecessary application requests in the secondary region.</p>
<p>One catch with this setup is: physics. Imagine we’re handling a large replicated write — say, an HTTP POST of a large recipe entry in Santiago, Chile. Something that can happen is that a request to read that entry back from Santiago can race the replication of the write from Paris, and lose. You see this pattern, “<em>create-and-redirect-to-show</em>”, somewhat regularly in Rails apps, and if you break it, you can get a poor user experience.</p>
<p>To prevent this, <strong class='font-[550] text-navy-950'>replayed requests set a configurable time threshold in a cookie</strong>. Requests arriving within the threshold sent by the browser will be sent to the primary region. This is a simple but valuable trade-off: a temporary performance penalty in exchange for consistency. Remember, we assume that most uses of the application won’t write at all; the worst case isn’t terrible, and the common case is very fast. It’s usually the right trade.</p>
<p>Curiously, this approach mirrors the Rails default implementation of <a href='https://edgeguides.rubyonrails.org/active_record_multiple_databases.html#activating-automatic-connection-switching' title=''>read/write splitting between primary and replica databases</a>.</p>
<h2 id='i-rack' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#i-rack' aria-label='Anchor'></a>I ❤️ Rack</h2>
<p>Apart from Fly.io’s magic, the Rack standard made this gem a cinch to implement. Rack is one of the major successes of the Ruby and Rails development environment. It’s underappreciated and deserves more appreciation, so here’s some love.</p>
<p>Most web apps share a lot of common behavior in marshaling, unmarshaling, validating, and routing requests. These are the basic features that a web framework provides, and why frameworks are so popular. It used to be difficult (and on some platforms it still is) to change those behaviors: you had to change your application, or, worse, the framework itself to accomplish it.</p>
<p>Python’s WSGI was probably the first standard aimed at solving this problem. Rack came shortly after, inspired by WSGI. Both provide a simple, elegant interface for inserting common behavior between web servers and applications. This also happens to be a great way to simplify framework-specific behavior.</p>
<p>Try typing <code>rails middleware</code> in a Rails app production environment:</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-b9g9n2wd"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-b9g9n2wd"><span class="n">use</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">HostAuthorization</span>
<span class="n">use</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">SSL</span>
<span class="n">use</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Sendfile</span>
<span class="n">use</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">Static</span>
<span class="n">use</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">Executor</span>
<span class="n">use</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Cache</span><span class="o">::</span><span class="no">Strategy</span><span class="o">::</span><span class="no">LocalCache</span><span class="o">::</span><span class="no">Middleware</span>
<span class="n">use</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Runtime</span>
<span class="n">use</span> <span class="no">Rack</span><span class="o">::</span><span class="no">MethodOverride</span>
<span class="n">use</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">RequestId</span>
<span class="n">use</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">RemoteIp</span>
<span class="n">use</span> <span class="no">Rails</span><span class="o">::</span><span class="no">Rack</span><span class="o">::</span><span class="no">Logger</span>
<span class="n">use</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">ShowExceptions</span>
<span class="n">use</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">DebugExceptions</span>
<span class="n">use</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">ActionableExceptions</span>
<span class="n">use</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">Callbacks</span>
<span class="n">use</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">Cookies</span>
<span class="n">use</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">Session</span><span class="o">::</span><span class="no">CookieStore</span>
<span class="n">use</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">Flash</span>
<span class="n">use</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">ContentSecurityPolicy</span><span class="o">::</span><span class="no">Middleware</span>
<span class="n">use</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">PermissionsPolicy</span><span class="o">::</span><span class="no">Middleware</span>
<span class="n">use</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Head</span>
<span class="n">use</span> <span class="no">Rack</span><span class="o">::</span><span class="no">ConditionalGet</span>
<span class="n">use</span> <span class="no">Rack</span><span class="o">::</span><span class="no">ETag</span>
<span class="n">use</span> <span class="no">Rack</span><span class="o">::</span><span class="no">TempfileReaper</span>
<span class="n">run</span> <span class="no">Cookherenow</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">routes</span>
</code></pre>
</div>
</div>
<p>Exception handling, caching, session management, cookie encryption, static file delivery - all implemented as Rack middleware. Building apps this way provides a clear path for a request to reach an application, and more importantly, a standard way to <em>insert middlewares at a specific location.</em> The framework is now programmable.</p>
<p>The <code>fly-ruby</code> gem implements two Rack “middlewares”. It’s idiomatic, and easy to shoplift (from, say, <a href='https://github.com/getsentry/sentry-ruby' title=''>Sentry’s exception handling library</a>).</p>
<h2 id='what-about-background-jobs' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#what-about-background-jobs' aria-label='Anchor'></a>What about background jobs?</h2>
<p>Background jobs are a core piece of infrastructure for most Rails apps. Naturally, they’ll need to write to the database.Restricting worker processes to the primary region is the simplest way to handle such jobs in a multi-region scenario.</p>
<p>But if we’re using a database - like Postgres or Redis - to store the jobs, <em>queuing up the job itself will be slow</em> from secondary regions. If we enqueue lots of jobs in GET requests, this performance loss could offset our gains.</p>
<p>Furthermore, some apps - like <a href='https://www.discourse.org' title=''>Discourse</a> - run smaller background jobs <a href='https://github.com/discourse/discourse/blob/main/lib/scheduler/defer.rb#L85' title=''>in the web process itself</a>. Both scenarios need to write the primary database without relying on HTTP trickery.</p>
<p>For example, we might add code to <code>fly-ruby</code> like this.</p>
<div class="highlight-wrapper group relative ruby">
<button
type="button"
class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-wrap-target="#code-n8xcbafq"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950">
Wrap text
</span>
</button>
<button
type="button"
class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
data-copy-target="sibling"
>
<svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg>
<span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950">
Copy to clipboard
</span>
</button>
<div class='highlight relative group'>
<pre class='highlight '><code id="code-n8xcbafq"><span class="no">Fly</span><span class="p">.</span><span class="nf">on_primary</span> <span class="k">do</span>
<span class="no">Recipes</span><span class="p">.</span><span class="nf">transform</span>
<span class="k">end</span>
</code></pre>
</div>
</div>
<p>The Rails support for read/write splits takes a similar path to <a href='https://edgeguides.rubyonrails.org/active_record_multiple_databases.html#using-manual-connection-switching' title=''>force a specific database connection</a>.</p>
<h2 id='where-this-breaks-down' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#where-this-breaks-down' aria-label='Anchor'></a>Where this breaks down</h2>
<p>Some complex Rails applications make this kind of setup difficult, like <a href='https://www.discourse.org' title=''>Discourse</a>.</p>
<p><strong class='font-[550] text-navy-950'>Some apps write on every request</strong>. Think about things like lazy authentication session token refresh, or touching a user’s <code>last_seen</code> attribute. These generate unexpected writes, and, worse, waste cycles on regional app servers.</p>
<p>Moving work like this to a background job is a fine solution to this problem. It also happens to be a best practice for keeping applications performant and resilient. So if you can do this, you should.</p>
<p>Background jobs in Rails apps without infrastructure support for jobs might seem like a pain. But it doesn’t have to be. You could implement the <a href='https://github.com/rails/rails/blob/main/activejob/lib/active_job/queue_adapters/async_adapter.rb' title=''>ActiveJob in-memory queue</a> for jobs you would not mind losing on restart.</p>
<p><strong class='font-[550] text-navy-950'>Complex interactions with other data stores may slow requests down.</strong> By default, Discourse backs statistics and logs into Redis, and reads and writes to it on every request. This can be tricky to deal with in a global deployment. Solutions like read/write splitting may be useful here, but they’re not “just install this gem”-simple to implement.</p>
<p><strong class='font-[550] text-navy-950'>Relying on catching read-only exceptions could lead to inconsistent data.</strong> For example, a visit counter being incremented in Redis <em>before the Postgres exception is raised</em> would be bumped twice: once in the secondary region request, and again in the replayed primary region request. Most apps aren’t going to care about this, but you want to be aware of it.</p>
<p><strong class='font-[550] text-navy-950'>Large multipart file uploads might be doubly slow</strong> if they’re replayed <em>after</em> the browser upload completes.</p>
<h2 id='whats-next' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#whats-next' aria-label='Anchor'></a>What’s next?</h2>
<p><a href='https://fly.io/blog/last-mile-redis/' title=''>Region-local Redis caches</a> would be dope. For Rails apps, this could mean that the common approach of fragments or Russian-doll caching could get a boost at the global level without much work.</p>
<p>And more adapters! Adapters for <a href='https://github.com/superfly/fly-node' title=''>Nodejs/Express</a>, Phoenix, Django, and friends. They’re totally doable and you should <a href='https://community.fly.io/' title=''>get in touch</a> if you like these kind of projects.</p>
/ruby-dispatch/run-ordinary-rails-apps-globally/assets/run-rails-globally-thumbnail.webp