<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[All about code - Ruby and Rails technical content written by Lucian Ghinda]]></title><description><![CDATA[I write here quick thoughts, ideas, tips, and learnings about programming, programmers, and building software.  Most of my focus is on Ruby, Rails, Hotwire, and everything about web applications.]]></description><link>https://allaboutcoding.ghinda.com</link><generator>RSS for Node</generator><lastBuildDate>Sat, 11 Apr 2026 07:42:18 GMT</lastBuildDate><atom:link href="https://allaboutcoding.ghinda.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Two Agent Skills to Help With Prompt Security]]></title><description><![CDATA[When you build a product that uses LLMs and prompts, security becomes a specific kind of hard problem.
Simon Willison described the core risk well in The Lethal Trifecta. When three things combine in ]]></description><link>https://allaboutcoding.ghinda.com/two-agent-skills-to-help-with-prompt-security</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/two-agent-skills-to-help-with-prompt-security</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[agentic AI]]></category><category><![CDATA[skills]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Tue, 07 Apr 2026 04:48:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fbe0c248caf5b2fd777f7f8/29aed759-e7f2-4e01-a3a5-945967ce17a6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When you build a product that uses LLMs and prompts, security becomes a specific kind of hard problem.</p>
<p>Simon Willison described the core risk well in <a href="https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/">The Lethal Trifecta</a>. When three things combine in a system: access to private data, exposure to untrusted content, and the ability to communicate externally, the situation becomes dangerous.</p>
<p>The reason is structural: LLMs follow instructions in content. That is what makes them useful. It is also what makes them exploitable. An attacker embeds instructions in a document, an email, a webpage, and the model may execute them.</p>
<p><strong>Prompt injection</strong> is the number one vulnerability in LLM applications according to <a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/">OWASP's Top 10 for LLMs</a>. And unlike SQL injection, where patterns are well-defined and tools are mature, LLM security does not yet have a comparable development workflow.</p>
<p>I tried to compile some best practices into two Claude Code skills, or better said, agentic skills because they should work with multiple agent harnesses.</p>
<p>I want to be clear upfront: this will not protect you as well as a static analysis tool would. LLM-based security review is not the same as deterministic rule checking. What these skills do is give you a structured checklist, grounded in authoritative sources, that you can run as part of your normal workflow. Still even if you use these skills you can still be open to prompt injection or other possible attack vectors. But I think these are a good start or at least a better start.</p>
<p>I am of course open to PRs and changes at <a href="https://github.com/lucianghinda/llm-prompts-skill">https://github.com/lucianghinda/llm-prompts-skill</a> if you discover better heuristics that we can apply to improve the security of LLMs prompts.</p>
<h2>The Problem</h2>
<p>Think about what "checking for prompt injection" actually means in practice. It is not one thing. The <a href="https://cheatsheetseries.owasp.org/cheatsheets/Prompt_Injection_Prevention_Cheat_Sheet.html">OWASP LLM01:2025 checklist</a> has 23 items just for direct injection. <a href="https://atlas.mitre.org">MITRE ATLAS</a> adds 12 mitigation controls. <a href="https://github.com/NVIDIA/NeMo-Guardrails">NVIDIA NeMo Guardrails</a> covers another 16 implementation patterns for RAG pipelines and jailbreak detection.</p>
<p>That is 51 distinct checks, and the list grows every time a new attack vector is discovered.</p>
<p>We have linters for code style, static analysis for bugs, dependency scanners for known CVEs. We have nothing comparable for LLM security that runs as part of a normal development workflow. Knowing about prompt injection in the abstract and having a structured way to check for it are two different things.</p>
<p>So I tried to build something useful here.</p>
<h2>What I Built</h2>
<p>Two companion skills, designed to be used with the Claude Code CLI or Codex or OpenCode or any kind of agentic harness that uses skills.</p>
<p><code>llm-prompts:reviewer</code> — point it at any codebase and it runs 51 checks across OWASP, MITRE ATLAS, and NeMo Guardrails. Every finding is rated CRITICAL / HIGH / MEDIUM / LOW with a precise location in your code and concrete remediation guidance. Of course when I say "running the 51 checks" I mean the LLM will try to read the code, understand it and check it against those checks. This is not a static analysis tool.</p>
<p><code>llm-prompts:builder</code> — answer 6 questions about your integration (role, task, data sources, output usage, language, architecture) and it generates a secure system prompt plus scaffolding code. Every generated line is annotated with the check ID it satisfies: <code># WHY: [O-7]</code>.</p>
<p>Here is what a reviewer report looks like:</p>
<pre><code class="language-markdown">PROMPT INJECTION SECURITY REVIEW
=================================
Scope:    codebase
CRITICAL: 3 fail / 2 pass
HIGH:     5 fail / 4 pass

TOP RISKS
---------
1. [O-7] No structured prompt separation
   Risk: User input is concatenated directly into the prompt
         — attacker can override system instructions
   Location: app.py:42
   Remediation: Wrap user input in a USER_DATA_TO_PROCESS block
                with --- delimiters...
</code></pre>
<h2>The 51 Checks, Briefly</h2>
<p>The framework is grounded in three authoritative sources.</p>
<p><strong>OWASP LLM01:2025 (O-1 through O-23)</strong> covers the fundamentals: input validation, injection pattern detection, typoglycemia defenses (deliberately misspelled words used to bypass keyword filters), the <a href="https://arxiv.org/abs/2402.06363">StruQ structured prompt separation</a> pattern, output validation, system prompt leakage, HTML/Markdown injection in outputs, access control, rate limiting, logging, and alerting.</p>
<p><strong>MITRE ATLAS (M-1 through M-12)</strong> maps to their adversarial AI threat framework: GenAI Guardrails (AML.M0020), GenAI Guidelines (AML.M0021), Human-in-the-Loop gates (AML.M0029), Tool Invocation restrictions (AML.M0030), Memory Hardening (AML.M0031).</p>
<p><strong>NeMo Guardrails patterns (N-1 through N-16)</strong> go deeper into RAG-specific risks: pre-indexing document validation, retrieval filtering, source attribution, heuristic and classifier-based jailbreak detection, PII detection, and three-layer defense configuration.</p>
<p>Each finding in the report maps to one of these check IDs so when you fix something, you know exactly which risk you addressed.</p>
<p>Of course I feel the need to say again that this is not a static analysis tool but an LLM so it might skip some of these checks or decide that they pass even if they should not. I see this as a starting point and you should make sure to manually review the prompts.</p>
<h2>The Builder: Focused on security</h2>
<p>I think the reviewer is the more immediately useful skill, but the builder is more interesting to me as a design idea.</p>
<p>The insight is this: it is easier to build something secure from the start than to retrofit security onto existing code. The builder operationalizes that by generating code where every defense is intentional and documented.</p>
<p>You answer the six questions, and it generates in Ruby and if you need to in Python:</p>
<ul>
<li><p>A <strong>system prompt</strong> with all five OWASP structural elements present</p>
</li>
<li><p><code>guardrails.rb</code> with InputGuardrail (length limits, encoding detection, injection pattern matching, typoglycemia defense) and OutputGuardrail (leakage detection, PII scanning, HTML injection detection)</p>
</li>
<li><p><code>integration.rb</code> with rate limiting, structured logging, and the main request handler</p>
</li>
<li><p><code>tools.rb</code> if you chose tool calling — parameterized queries, allowlisted tables, human-approval gates for destructive actions</p>
</li>
<li><p><code>rag.rb</code> if you chose RAG — pre-indexing validation, retrieval filtering, source provenance tracking</p>
</li>
</ul>
<p>For JavaScript/TypeScript, the builder generates the prompt construction function.</p>
<p>Every generated line of code includes a comment like <code># WHY: [O-7]</code>. That annotation does two things. First, it makes the purpose of the code explicit a future developer can see that a particular block exists to prevent structured prompt injection, not as an arbitrary formatting choice. Second, it makes the code reviewable against the framework. You can grep for <code>WHY: [O-7]</code> and immediately see all the places where that specific check is addressed.</p>
<p>This is what I mean by <strong>security that documents itself</strong>. The check IDs become a shared vocabulary between the code, the review report, and the security team without any additional documentation effort.</p>
<h2>A Reference Implementation</h2>
<p>The repo includes two fixture apps in <code>tests/fixtures/</code>:</p>
<ul>
<li><p><code>defended-app/</code> — a complete Flask + OpenAI chatbot that demonstrates every defense. This is the canonical reference the builder generates from.</p>
</li>
<li><p><code>vulnerable-app/</code> — the same app, stripped of all guardrails. Raw string concatenation, no validation, no structured separation.</p>
</li>
</ul>
<p>I found having both versions genuinely useful while building this. The vulnerable version is not just for illustration — it is the test target. Seeing what an insecure app looks like at the code level makes it much easier to recognize the same pattern in your own codebase.</p>
<h2>Testing</h2>
<p>I wanted to be able to trust that the skills catch what they claim to catch.</p>
<p>The test suite runs both skills against the fixture apps using the real Claude CLI and validates that the output contains the expected PASS/FAIL verdicts:</p>
<pre><code class="language-shell"># Reviewer against both fixtures
./tests/run-tests.sh --claude-only vulnerable-app
./tests/run-tests.sh --claude-only defended-app

# Builder scenarios
./tests/test-builder-claude.sh basic-chatbot
./tests/test-builder-claude.sh rag-assistant
./tests/test-builder-claude.sh tool-calling-agent
</code></pre>
<p>Each test invokes real LLM calls, so they take 60-180 seconds. A test fails only when a CRITICAL or HIGH verdict is missing — MEDIUM and LOW mismatches are reported as warnings.</p>
<p>The supported languages for code templates are Python, JavaScript/TypeScript, Ruby, and Go.</p>
<h2>What This Is and Is Not</h2>
<p>I want to be honest about the limits here.</p>
<p>LLM-based security review is not deterministic. A static analysis tool will catch every instance of a pattern, every time, with no variation. These skills run a checklist using an LLM, so results can vary. Willison's point in the lethal trifecta piece is worth repeating: in security contexts, 95% protection is not protection. A missed check is a real gap.</p>
<p>So I am not arguing this replaces a security audit. A dedicated penetration tester will find things this framework misses. I am not even arguing these skills are equivalent to a good static analyzer.</p>
<p>What I think they do: they raise the floor, especially for solo developers and small teams building LLM features without dedicated security expertise. The 51 checks give you a structured starting point you can run during development, not just at the end. The check IDs give you a shared vocabulary. And the annotations give you documentation you did not have to write separately.</p>
<p>It is more comfortable to identify gaps in a review report than to explain them after a production incident.</p>
<p>The project is open source. If you find that the builder generates code that does not hold up against your real requirements, or if you think I got a check wrong, I would genuinely like to know. Please open an issue.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/">The Lethal Trifecta for AI Agents</a> by Simon Willison</p>
</li>
<li><p><a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/">OWASP Top 10 for Large Language Models 2025</a></p>
</li>
<li><p><a href="https://cheatsheetseries.owasp.org/cheatsheets/Prompt_Injection_Prevention_Cheat_Sheet.html">OWASP Prompt Injection Prevention Cheat Sheet</a></p>
</li>
<li><p><a href="https://atlas.mitre.org/">MITRE ATLAS — Adversarial Threat Landscape for AI Systems</a></p>
</li>
<li><p><a href="https://github.com/NVIDIA/NeMo-Guardrails">NVIDIA NeMo Guardrails</a></p>
</li>
<li><p><a href="https://arxiv.org/abs/2402.06363">StruQ: Defending Against Prompt Injection with Structured Queries</a></p>
</li>
<li><p><a href="https://github.com/lucianghinda/llm-prompt-injection-reviewer">llm-prompt-injection-reviewer on GitHub</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[superpowers-ruby v6.1.0: a Rails upgrade skill]]></title><description><![CDATA[One new skill in this release: rails-upgrade.
Why I built this
Seeing the skills for upgrading a Rails app released in the last months with the one from OmbuLabs/FastRuby.io being released last week:
]]></description><link>https://allaboutcoding.ghinda.com/superpowers-ruby-v6-1-0-a-rails-upgrade-skill</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/superpowers-ruby-v6-1-0-a-rails-upgrade-skill</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[upgrade]]></category><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Thu, 02 Apr 2026 12:27:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fbe0c248caf5b2fd777f7f8/555014f0-146d-4240-9cef-edf8c96da592.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One new skill in this release: <code>rails-upgrade</code>.</p>
<h2>Why I built this</h2>
<p>Seeing the skills for upgrading a Rails app released in the last months with the one from OmbuLabs/FastRuby.io being released last week:</p>
<ul>
<li><p><a href="https://github.com/ombulabs/claude-code_rails-upgrade-skill">OmbuLabs/FastRuby.io</a>: detection patterns, gem compatibility data, upgrade methodology</p>
</li>
<li><p><a href="https://github.com/maquina-app/rails-upgrade-skill">maquina-app/rails-upgrade-skill</a> by Mario Alberto Chávez Cárdenas: breaking changes reference tables, deprecation timeline</p>
</li>
</ul>
<p>I got thinking that maybe it is time for superpowers-ru<a href="https://github.com/lucianghinda/superpowers-ruby">superpowers-ruby</a>by to have such a skill.</p>
<h2>The workflow</h2>
<p>The skill runs a six-step process, but three of those steps are hard gates: the agent cannot proceed past them unless a condition is met.</p>
<p>The first gate is a green test suite. The agent runs the tests before touching anything. If tests are failing before the upgrade starts, failures after the upgrade are ambiguous. You cannot tell what you broke from what was already broken. I have made that mistake before.</p>
<p>The second gate checks <code>config.load_defaults</code>. If there is a <code>new_framework_defaults_*.rb</code> file from a previous upgrade with unresolved settings, that needs to be resolved before starting another upgrade. Stacking unresolved defaults makes the transition plan unreliable.</p>
<p>The third gate is explicit approval. Before the agent touches any files, it compiles an upgrade report: configuration changes, code issues by severity, gem updates, and a load_defaults transition plan. I have to approve that report before anything is modified. Some upgrades are a few dependency bumps. Others involve weeks of work. I want to know which one I am starting before it has already begun.</p>
<p>Between the gates, the skill fetches a live configuration diff from GitHub:</p>
<pre><code class="language-plaintext">https://api.github.com/repos/railsdiff/rails-new-output/compare/v{FROM}...v{TO}
</code></pre>
<p>This is the data behind railsdiff.org. Every config file added, modified, or removed between two Rails versions, fetched on each run. I did not want to maintain a static list that would go stale the moment a new version shipped.</p>
<p>For code-level detection, the agent searches the codebase directly with Grep. It looks for things like <code>Rails.application.secrets</code>, <code>params ==</code> comparisons, <code>config.cache_classes</code>, <code>ActiveRecord::Base.connection</code> without a block. No script generation, no round-trip. Findings come back with file:line references.</p>
<h2>Reference files</h2>
<p>I included seven reference files in the <code>references/</code> directory:</p>
<ul>
<li><p><strong>breaking-changes.md</strong>: HIGH/MEDIUM/LOW tables for all seven version pairs from 5.2 through 8.1</p>
</li>
<li><p><strong>detection-patterns.md</strong>: Grep and Glob patterns organized by version pair</p>
</li>
<li><p><strong>gem-compatibility.md</strong>: ~50 popular gems with the minimum required version for each Rails release</p>
</li>
<li><p><strong>load-defaults-guide.md</strong>: every <code>config.load_defaults</code> setting from 5.2 through 8.1 with risk tiers</p>
</li>
<li><p><strong>deprecation-timeline.md</strong>: when features were deprecated, when they were removed, what replaced them</p>
</li>
<li><p><strong>dual-boot-guide.md</strong>: complete <code>next_rails</code> setup including the <code>NextRails.next?</code> pattern and CI config</p>
</li>
<li><p><strong>troubleshooting.md</strong>: common upgrade errors and their fixes</p>
</li>
</ul>
<p>I originally considered splitting the load_defaults guide and dual-boot setup into separate companion skills. I decided against it. Having everything in one place means there is nothing extra to install, and the reference material stays in sync with the skill itself.</p>
<h2>The fetch-changelogs script</h2>
<p>There is also a small utility at <code>scripts/fetch-changelogs.sh</code>. It fetches CHANGELOG entries for a specific Rails version from GitHub and writes them to files, one per component and a consolidated version.</p>
<pre><code class="language-bash">./scripts/fetch-changelogs.sh 8.1.0
./scripts/fetch-changelogs.sh 8.1.0 ./changelogs
./scripts/fetch-changelogs.sh --list-versions
</code></pre>
<p>Rails keeps separate CHANGELOGs per component: Action Cable, Active Record, Action Pack, and nine others. The script fetches all twelve and extracts just the section relevant to the requested version. I find it useful to read the raw changelog before running the upgrade workflow.</p>
<h2>Attribution</h2>
<p>I want to be clear about where this came from. Two existing skills did the hard work of organizing the reference material I use here:</p>
<ul>
<li><p><a href="https://github.com/ombulabs/claude-code_rails-upgrade-skill">OmbuLabs/FastRuby.io</a>: detection patterns, gem compatibility data, upgrade methodology</p>
</li>
<li><p><a href="https://github.com/maquina-app/rails-upgrade-skill">maquina-app/rails-upgrade-skill</a> by Mario Alberto Chávez Cárdenas: breaking changes reference tables, deprecation timeline</p>
</li>
</ul>
<p>Both are MIT-licensed and both are excellent. If you are looking for a Rails upgrade skill, I genuinely recommend looking at them first and using them directly. At the moment they are more battle tested than this skill and also those are skills from people with a long track record. Ombulabs/FastRuby are having the biggest experience with Rails upgrades so I think their skill is the best one so far.</p>
<p>Here is what I added/changed in this skill:</p>
<ol>
<li><p><strong>Self-contained, no external dependencies.</strong> OmbuLabs requires three companion skills installed. My version works anywhere Claude Code works.</p>
</li>
<li><p><strong>Live config diffs from railsdiff.org.</strong> Neither source skill fetches configuration changes from the actual railsdiff.org data. The GitHub API call on every run means the diff is always current.</p>
</li>
<li><p><strong>No script generation, no MCP dependencies.</strong> Nothing extra to install or configure.</p>
</li>
<li><p><strong>Three hard gates.</strong> Green test suite, clean <code>config.load_defaults</code>, and explicit approval before any execution. Not soft suggestions but actual stops.</p>
</li>
<li><p><strong>A pragmatic version range.</strong> Mario covers 7.0+, OmbuLabs goes back to 2.3. I chose 5.2 through 8.1 because that is the practical range for most apps still being upgraded today.</p>
</li>
<li><p><strong>A linear six-step workflow.</strong> OmbuLabs has eight steps with numbering gaps. I wanted a clear sequence with no ambiguity about what comes next.</p>
</li>
<li><p><strong>An integrated upgrade report.</strong> railsdiff config data, codebase detection findings, and an effort estimate in a single template, before any approval is requested.</p>
</li>
<li><p><strong>Cross-references to the official Rails upgrade guide.</strong> Neither source skill does this explicitly.</p>
</li>
</ol>
<p>If my version does not fit your workflow, <strong>their skills are the right starting point for building your own or use them as they are</strong>.</p>
<p>I think this is one of the more interesting things about working with LLMs on developer tooling. You can take reference material that other people have carefully organized, mix it with your own workflow preferences, and get something that fits exactly how you work. The raw material was already there. I just combined it differently.</p>
<p>My additions are small compared to what both teams built. I am grateful to them for releasing it all under MIT.</p>
<h2>No migration needed</h2>
<p>Purely additive release. If you are on 6.0.x, reinstall the plugin or update it to pick up the new skill. Or wait until you actually need to upgrade Rails.</p>
<p>If you try it and find something that does not work well, I would love to hear about it.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://github.com/lucianghinda/superpowers-ruby">superpowers-ruby v6.1.0</a></p>
</li>
<li><p><a href="https://github.com/ombulabs/claude-code_rails-upgrade-skill">OmbuLabs/FastRuby.io rails-upgrade-skill</a></p>
</li>
<li><p><a href="https://github.com/maquina-app/rails-upgrade-skill">maquina-app/rails-upgrade-skill</a> by Mario Alberto Chávez Cárdenas</p>
</li>
<li><p><a href="https://railsdiff.org">railsdiff.org</a>: the config diff tool the skill uses</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Superplanning: a unified planning skill for Claude Code]]></title><description><![CDATA[I noticed I kept switching between planning skills mid-session. One skill had the best forcing questions. Another had the best implementation structure. A third had the best review discipline. Also I ]]></description><link>https://allaboutcoding.ghinda.com/superplanning-a-unified-planning-skill-for-claude-code</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/superplanning-a-unified-planning-skill-for-claude-code</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[planning]]></category><category><![CDATA[Products]]></category><category><![CDATA[mvp]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Mon, 30 Mar 2026 05:11:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fbe0c248caf5b2fd777f7f8/2b26969d-9f13-4e7c-bdc8-82cd40f52321.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I noticed I kept switching between planning skills mid-session. One skill had the best forcing questions. Another had the best implementation structure. A third had the best review discipline. Also I think I have too many plugin collections and each seem to have a planning skill somehow.</p>
<p>So I analyzed 48+ planning skills across five repositories and combined the strongest elements into one flow. That is superplanning.</p>
<h2>The problem with using multiple planning skills</h2>
<p>When I started using Claude Code for serious product work, I had at least five planning-related skills available: <code>ce:brainstorm</code> for requirements, <code>ce:plan-beta</code> for implementation units, <code>office-hours</code> for pressure-testing ideas, <code>plan-ceo-review</code> for scope discipline, <code>plan-eng-review</code> for architecture review. These were the main ones.</p>
<p>Each one did something the others didn't. But using them together meant manually carrying findings from one session into the next, re-establishing context every time, and deciding myself which skill to reach for first.</p>
<h2>What superplanning does</h2>
<p><strong>Superplanning</strong> is a single Claude Code skill that handles three scenarios: brainstorming an idea, planning a new product, planning a new feature for an existing codebase all using a shared 7-phase flow.</p>
<pre><code class="language-plaintext">Phase 0: INTAKE &amp; ROUTE     — detect mode, check for prior work, classify scope
Phase 1: GROUND             — research context
Phase 2: CHALLENGE &amp; EXPLORE — pressure test the premise before any solution work
Phase 3: DEFINE             — produce requirements doc or product docs
Phase 4: STRUCTURE          — break work into implementation units
Phase 5: VALIDATE           — multi-persona review (CEO → Design → Eng)
Phase 6: DEEPEN             — targeted research on weakest sections (conditional)
Phase 7: HAND OFF           — summary, artifacts, recommended next step
</code></pre>
<p>You don't choose which skill to use. Phase 0 detects what you're doing and routes accordingly.</p>
<h2>Three things I found worth preserving</h2>
<p>There were 15 techniques worth integrating from the source skills. I want to explain three of them, because they are the ones I did not find elsewhere.</p>
<p><strong>Stage-routed forcing questions.</strong> The six forcing questions from <code>office-hours</code> — Demand Reality, Status Quo, Desperate Specificity, Narrowest Wedge, Observation &amp; Surprise, Future-Fit are not all asked in every session. They route by product stage. Pre-product gets Q1–Q3. Teams with paying customers get Q4–Q6. Asking a team with 500 paying customers to prove demand is insulting and irrelevant. The routing makes the questions land where they actually hurt.</p>
<p><strong>Anti-sycophancy as structural constraints, not style guidance.</strong> "Be critical" is tone advice and models ignore it. The stronger approach is naming the specific phrases that signal sycophancy and banning them:</p>
<ul>
<li><p>"That's interesting" → take a position instead</p>
</li>
<li><p>"There are many ways to think about this" → pick one, state what evidence would change your mind</p>
</li>
<li><p>"That could work" → say whether it WILL work and what evidence is missing</p>
</li>
</ul>
<p>The rule is: take a position AND state what evidence would change it. If the behavior you would use to hedge is named and banned, the model cannot accidentally use it.</p>
<p><strong>Confidence gap scoring.</strong> After a plan is drafted, each section is scored trigger count (checklist problems found) + risk bonus + critical-section bonus. Only the top 2–5 sections are deepened with targeted research. Generic "improve the plan" instructions produce uniformly padded plans. Scoring concentrates improvement where the plan is actually weakest.</p>
<p>At the end you will have a series of documents under <code>docs</code> that you can use in any future planning session.</p>
<h2>What each source contributed</h2>
<p>I want to be clear about attribution because the useful techniques here came from other people's work. This is based on their experience and how they shared that experience through agent skills.</p>
<p>The flow spine and requirements document template with stable IDs (R1, R2...) came from <code>ce:brainstorm</code> in compound-engineering. The implementation unit structure (each unit specifying exact file paths, requirements trace, test scenarios, verification) came from <code>ce:plan-beta</code>, also compound-engineering. The forcing questions and anti-sycophancy rules came from <code>office-hours</code> in gstack. The scope modes came from <code>plan-ceo-review</code> in gstack. The shadow path tracing technique came from <code>feasibility-reviewer</code> in compound-engineering, elevated to a Prime Directive in the CEO review phase. The confidence gap scoring came from <code>deepen-plan-beta</code> in compound-engineering. The multi-persona document review came from <code>document-review</code>, also compound-engineering.</p>
<p>The full attribution table with what was used and how it was adapted is in <a href="https://github.com/lucianghinda/superplanning/blob/main/SOURCES.md">SOURCES.md</a>.</p>
<h2>Installation</h2>
<p>Copy <code>skills/superplanning/</code> into your global Claude skills directory:</p>
<pre><code class="language-bash">cp -r skills/superplanning ~/.claude/skills/superplanning
</code></pre>
<p>Or point <code>--plugin-dir</code> at the repo root for a single session:</p>
<pre><code class="language-bash">claude --plugin-dir /path/to/superplanning
</code></pre>
<p>Or copy <code>skills/superplanning/</code> into your project's <code>.claude/skills/</code> directory for project-local access:</p>
<pre><code class="language-plaintext">your-project/
└── .claude/
    └── skills/
        └── superplanning/
</code></pre>
<p>The skill works in other agentic tools too (Codex, Gemini), not just Claude Code.</p>
<p>The skill triggers automatically from phrases like "brainstorm this idea", "plan this from scratch", "is this worth building". You can also invoke it explicitly: <code>superplanning, here's my idea: [describe it]</code>.</p>
<p>I think this is genuinely useful not because the 7-phase flow is clever, but because having the challenge phase happen before the define phase, automatically, without you having to remember to run a separate skill, changes what you end up with.</p>
<p>If you try it and find something that doesn't work as described, I would be glad to hear about it.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://github.com/lucianghinda/superplanning">superplanning on GitHub</a></p>
</li>
<li><p><a href="https://github.com/adrianthedev/compound-engineering">compound-engineering-plugin</a> — source for <code>ce:brainstorm</code>, <code>ce:plan-beta</code>, <code>ce:ideate</code>, <code>deepen-plan-beta</code>, <code>document-review</code>, <code>feasibility-reviewer</code></p>
</li>
<li><p><a href="https://github.com/gschlager/gstack">gstack skills</a> — source for <code>office-hours</code>, <code>autoplan</code>, <code>plan-ceo-review</code>, <code>plan-eng-review</code>, <code>plan-design-review</code></p>
</li>
<li><p><a href="https://github.com/obie/superpowers">superpowers</a> — source for <code>brainstorming</code>, <code>writing-plans</code>, <code>subagent-driven-development</code></p>
</li>
<li><p><a href="https://github.com/agent-ops/agent-os">agent-os</a> — source for <code>plan-product</code>, <code>shape-spec</code></p>
</li>
<li><p><a href="https://github.com/obie/rails-claude-code">rails-claude-code</a> — source for <code>mvp-creator</code></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[superpowers-ruby v5.0.6: compound skills and Ruby idiom fixes]]></title><description><![CDATA[Here are some additions I did for superpowers-ruby collection of plugins and skills. A fork from the well known superpowers plugin where I added some skills from compound-engineering-plugin which I th]]></description><link>https://allaboutcoding.ghinda.com/superpowers-ruby-v5-0-6-compound-skills-and-ruby-idiom-fixes</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/superpowers-ruby-v5-0-6-compound-skills-and-ruby-idiom-fixes</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[llm]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Wed, 25 Mar 2026 11:21:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fbe0c248caf5b2fd777f7f8/f18ee4f5-14fc-4ef4-bfba-067c25f799bd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Here are some additions I did for <a href="https://github.com/lucianghinda/superpowers-ruby">superpowers-ruby</a> collection of plugins and skills. A fork from the well known superpowers plugin where I added some skills from <a href="https://github.com/EveryInc/compound-engineering-plugin">compound-engineering-plugin</a> which I think you should install it as it is.</p>
<h2>The compound skill</h2>
<p>I found that the compound skill from the folks at Every solves the problem of redisovering the same solution in various forms. The idea is simple: right after you and your agent fix something - while the context is still fresh - you capture what you actually did into a structured learning doc in docs/solutions/.</p>
<p>Next time you hit the same class of problem, the agent learns and uses it.</p>
<p>I think this is one of the most useful skills to add to a Ruby/Rails project. When you are working on a Rails app, you accumulate a lot of tribal knowledge - about how your specific setup works, what N+1 patterns are common in your models, how your Stimulus controllers are wired. The compound skill makes that knowledge persistent and searchable.</p>
<p>I <a href="https://github.com/lucianghinda/superpowers-ruby/tree/main/skills/superpowers-compound">adapted the skill</a> for superpowers-ruby from the original <a href="https://github.com/EveryInc/compound-engineering-plugin">compound-engineering-plugin</a>. The changes were mostly about conventions, reducing the frontmatter to name and description only, rewriting the description to be trigger-first, and compressing the content from 1901 to 690 words. The core idea is entirely from the Every team and I want to give them full credit for it.</p>
<h2>The companion: compound-refresh</h2>
<p>The second skill is <a href="https://github.com/lucianghinda/superpowers-ruby/tree/main/skills/superpowers-compound-refresh">superpowers:compound-refresh</a>. This one runs after refactors, Rails upgrades, or migrations - when the learnings you captured might have drifted from how the code actually works now. Based on the same skill from <a href="https://github.com/EveryInc/compound-engineering-plugin">compound-engineering-plugin</a>.</p>
<p>It has two modes. In interactive mode, it asks you one question at a time and leads with a recommendation. In autonomous mode (mode:autonomous), it processes everything without asking, marks ambiguous cases as stale, and generates a full report at the end.</p>
<p>There are four outcomes for each learning doc: Keep, Update, Replace, or Archive. The hardest distinction to get right is Update vs Replace - if you find yourself rewriting the solution section, that is Replace, not Update.</p>
<p>I also added a Common Mistakes section to this skill because I found during testing that agents would Archive learnings when the referenced files were gone - even when the problem domain was still active. The file being gone does not mean the problem went away.</p>
<h2>Ruby skill improvements</h2>
<p>I also revisited the ruby skill itself.</p>
<p>The original description started with "Ruby language conventions, idioms, and modern features (3.x+) for writing idiomatic Ruby code. Covers error handling patterns..." - and I found this was causing a problem.</p>
<p>When agents see a description that summarizes the content, they use it as confirmation they already know the topic. They answer from training memory instead of loading the skill body. For general Ruby questions this is fine, but the skill contains opinionated conventions that agents do not know by default - like the Weirich raise/fail distinction.</p>
<p>Here is what I mean. Ask an agent "should I use raise or fail?" without loading the skill and you get:</p>
<p>▎ "They are aliases in Ruby. Use whichever you prefer."</p>
<p>That is technically true. But the Weirich convention says: use fail when raising a new exception, reserve raise only for re-raising a caught one. It is a semantic signal to readers about intent. Agents only know this if they read the skill.</p>
<p>I rewrote the description to start with "Use when..." and added raise vs fail as a keyword so the skill surfaces for the exact questions it uniquely answers. I also added an Overview section and a Common Mistakes table</p>
<ul>
<li>including rescue Exception which catches SignalException and NoMemoryError and should almost never be used.</li>
</ul>
<h2>Ruby commit message skill</h2>
<p>The release also includes a smaller addition: a skill for writing Ruby-style git commit messages (PR #1). It covers tense, length limits, subject/body separation, and conventions for referencing Ruby classes, methods, and modules in commit subjects.</p>
<p>You can find all of this in superpowers-ruby v5.0.6. If you try the compound skills on a Ruby or Rails project and find something that does not work well, I would be glad to hear about it.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://github.com/lucianghinda/superpowers-ruby">superpowers-ruby v5.0.6</a></p>
</li>
<li><p><a href="https://github.com/EveryInc/compound-engineering-plugin">EveryInc/compound-engineering-plugin</a> — the original compound skill this was adapted from</p>
</li>
<li><p><a href="https://github.com/obra/superpowers">Jesse Vincent's superpowers</a> — the base this project is built on</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[3 New RuboCop Style Cops: SelectByKind, SelectByRange, PartitionInsteadOfDoubleSelect]]></title><description><![CDATA[While upgrading RuboCop in one project, I noticed three new style cops in v1.85.0. I took a close look at them and my recommendation is to enable them all.
Let's start.
The 3 Cops
The cops are:

Style]]></description><link>https://allaboutcoding.ghinda.com/3-new-rubocop-style-cops-selectbykind-selectbyrange-partitioninsteadofdoubleselect</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/3-new-rubocop-style-cops-selectbykind-selectbyrange-partitioninsteadofdoubleselect</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Rubocop]]></category><category><![CDATA[quality]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Thu, 19 Mar 2026 05:49:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fbe0c248caf5b2fd777f7f8/25818b55-1ace-4496-bd27-7a82e26a1090.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>While upgrading RuboCop in one project, I noticed three new style cops in <code>v1.85.0</code>. I took a close look at them and my recommendation is to enable them all.</p>
<p>Let's start.</p>
<h2>The 3 Cops</h2>
<p>The cops are:</p>
<ol>
<li><p><code>Style/SelectByKind</code> (<a href="https://github.com/rubocop/rubocop/pull/14808">PR #14808</a>)</p>
</li>
<li><p><code>Style/SelectByRange</code> (<a href="https://github.com/rubocop/rubocop/pull/14810">PR #14810</a>)</p>
</li>
<li><p><code>Style/PartitionInsteadOfDoubleSelect</code> (<a href="https://github.com/rubocop/rubocop/pull/14923">PR #14923</a>)</p>
</li>
</ol>
<p>Each is marked as correctable so if you run this you can automatically apply the changes.</p>
<h2>1) Style/SelectByKind</h2>
<p>This one rewrites class/module filtering from <code>select</code>/<code>reject</code> to <code>grep</code>/<code>grep_v</code>.</p>
<pre><code class="language-ruby"># before
array.select { |x| x.is_a?(Foo) }
array.reject { |x| x.is_a?(Foo) }

# after
array.grep(Foo)
array.grep_v(Foo)
</code></pre>
<p>From the PR discussion, two details mattered to me:</p>
<ul>
<li><p>The name changed from <code>SelectByClass</code> to <code>SelectByKind</code>, because modules are valid too, not only classes.</p>
</li>
<li><p>There is historical context: <code>Enumerable#grep</code> has existed in Ruby for decades, and class matching via <code>===</code> is old Ruby behavior, not a new trick.</p>
</li>
</ul>
<p>I am not saying <code>select { is_a? }</code> is bad code. I am arguing <code>grep(Foo)</code> signals intent faster once the team gets used to it.</p>
<h3>Why does <code>grep(Foo)</code> work at all?</h3>
<p><code>Enumerable#grep</code> filters by evaluating <code>pattern === element</code> for each element, where the pattern is always on the left. The left-hand operand decides what <code>===</code> means: <code>Class</code> defines it to call <code>is_a?</code>, <code>Range</code> defines it to call <code>cover?</code>, and <code>Regexp</code> defines it to call <code>match?</code>.</p>
<p>This is why <code>grep(Foo)</code>, <code>grep(1..10)</code>, and <code>grep(/pattern/)</code> all work the same way. The unifying idea is that <code>grep</code> is a pattern filter, and <code>===</code> is the protocol each pattern type implements.</p>
<blockquote>
<p><strong>One thing to watch:</strong> if you are using <code>find { |x| x.is_a?(Foo) }</code>, do not rewrite it to <code>grep(Foo).first</code>. The <code>find</code> version stops at the first match. The <code>grep</code> version builds the entire result array first, then takes the first element. The cop intentionally excludes <code>find</code>/<code>detect</code> for exactly this reason.</p>
</blockquote>
<h2>2) Style/SelectByRange</h2>
<p>This one rewrites range checks to <code>grep(range)</code> / <code>grep_v(range)</code>.</p>
<pre><code class="language-ruby"># before
array.select { |x| (1..10).cover?(x) }
array.reject { |x| (1..10).cover?(x) }

# after
array.grep(1..10)
array.grep_v(1..10)
</code></pre>
<p>This PR was explicitly inspired by <code>SelectByKind</code>, and follows the same philosophy: if you are pattern-filtering, use Ruby's pattern-filtering API directly.</p>
<p>For me, this is a consistency win more than anything else. If your codebase already uses <code>grep</code> for regexp or class matching, range matching with <code>grep</code> fits naturally. So if you enable the first cop I shared you should also enable this one.</p>
<h2>3) Style/PartitionInsteadOfDoubleSelect</h2>
<p>This one is a bit more practical.</p>
<pre><code class="language-ruby"># before
positives = array.select { |x| x &gt; 0 }
negatives = array.reject { |x| x &gt; 0 }

# after
positives, negatives = array.partition { |x| x &gt; 0 }
</code></pre>
<p>The PR went beyond the obvious case. During review, it added support for:</p>
<ul>
<li><p><code>&amp;:symbol</code> forms</p>
</li>
<li><p>mixed forms (<code>select(&amp;:positive?)</code> + <code>reject { |x| x.positive? }</code>)</p>
</li>
<li><p>structural negation pairs (<code>select { expr }</code> with <code>select { !expr }</code>, and same for <code>reject</code>)</p>
</li>
</ul>
<h2>To keep in mind: <code>===</code> Is Not Symmetric</h2>
<p>There is one thing I want to make explicit because it is easy to get wrong and the mistake is silent in case you are new to Ruby.</p>
<p><code>===</code> is a regular Ruby method called on the <strong>left-hand object</strong>. So <code>A === B</code> calls <code>A.===(B)</code></p>
<pre><code class="language-ruby">Integer === 42        # calls 42.is_a?(Integer)  → true
(1..10) === 5         # calls (1..10).cover?(5)   → true
/foo/   === "foobar"  # calls /foo/.match?("foobar") → true
</code></pre>
<p>If you reverse the order, you are calling <code>===</code> on a plain object: an integer instance, a string instance, a custom class instance. None of those override <code>===</code>. They fall through to <code>Object#===</code>, which is just <code>self == other</code>.</p>
<pre><code class="language-ruby">42 === Integer        # 42 == Integer  → false (always)
5  === (1..10)        # 5 == (1..10)   → false (always)
"foobar" === /foo/    # "foobar" == /foo/ → false (always)
</code></pre>
<p>The part to pay attention is that Ruby will return false and not raise an error because those objects are implementing the <code>===</code> comparison. It is not just what you might expect it to be. You just get the wrong answer.</p>
<p>Here is the trap in a realistic scenario:</p>
<pre><code class="language-ruby">data = [1, "hello", 2, "world", 3]

# correct
data.grep(Integer)                    # =&gt; [1, 2, 3]
data.select { |x| Integer === x }     # =&gt; [1, 2, 3]

# reversed: looks similar, silently broken
data.select { |x| x === Integer }     # =&gt; []
</code></pre>
<p>The reversed version returns an empty array every time, with no warning, as it is expected cause the object <code>x</code> is defining <code>===</code>.</p>
<p>You can find out that in these two cases there are two different methods executed:</p>
<pre><code class="language-ruby">1.method(:===) # =&gt; #&lt;Method: Integer#===(_)&gt;
Integer.method(:===) # =&gt; #&lt;Method: #&lt;Class:Integer&gt;(Module)#===(_)&gt;
</code></pre>
<p>The method <code>Integer#===</code> is <a href="https://docs.ruby-lang.org/en/master/Integer.html#method-i-3D-3D">defined</a> as an alias for <code>==</code> and says this:</p>
<blockquote>
<p>Returns whether self is numerically equal to other:</p>
</blockquote>
<p>But the method from <code>Module#===</code> is <a href="https://docs.ruby-lang.org/en/master/Module.html#method-i-3D-3D-3D">defined</a> as:</p>
<blockquote>
<p>Returns whether other is an instance of self, or is an instance of a subclass of self</p>
</blockquote>
<p>There is one edge case where reversal accidentally works by default: when both sides are strings.</p>
<pre><code class="language-ruby">"hello" === "hello"   # =&gt; true   (String#== compares values)
"hello" === "world"   # =&gt; false
</code></pre>
<p>This works because <code>String#==</code> compares string content. The moment you switch to a Regexp or a class, the reversal breaks.</p>
<pre><code class="language-ruby">/hello/ === "hello world"    # =&gt; true   (Regexp#match?)
"hello world" === /hello/    # =&gt; false  (String#== sees a non-String, returns false)
</code></pre>
<p><code>grep</code> protects you from this by always calling <code>pattern === element</code> with the pattern on the left. That is part of why reaching for <code>grep</code> is the right call, not just a style preference.</p>
<h2>Benchmarks I Ran</h2>
<p>I wrote <a href="https://gist.github.com/lucianghinda/6016e7bb0fce4425d49182b26712ccb9">three Ruby scripts</a> to sanity-check behavior and measure runtime:</p>
<ul>
<li><p><code>benchmark_select_by_kind.rb</code></p>
</li>
<li><p><code>benchmark_select_by_range.rb</code></p>
</li>
<li><p><code>benchmark_partition_instead_of_double_select.rb</code></p>
</li>
</ul>
<p>Each script first asserts semantic equivalence, then runs repeated benchmarks with <a href="https://github.com/evanphx/benchmark-ips"><code>benchmark-ips</code></a>.</p>
<pre><code class="language-bash">ruby benchmark_select_by_kind.rb
ruby benchmark_select_by_range.rb
ruby benchmark_partition_instead_of_double_select.rb
</code></pre>
<h3>Results on my machine</h3>
<ul>
<li><p><code>SelectByKind</code>: <code>grep(UserRecord)</code> reached <code>4.2 i/s</code> vs <code>2.6 i/s</code> for <code>select { is_a? }</code> (about <code>1.62x</code> throughput).</p>
</li>
<li><p><code>SelectByRange</code>: <code>grep(range)</code> and <code>grep_v(range)</code> reached <code>1.1 i/s</code> vs <code>0.9 i/s</code> for <code>cover?</code>-based filters (about <code>1.17x</code> throughput).</p>
</li>
<li><p><code>PartitionInsteadOfDoubleSelect</code>: <code>partition</code> reached <code>1.0 i/s</code> vs <code>0.6 i/s</code> for <code>select + reject</code> (<code>1.68x</code> throughput), because it walks once.</p>
</li>
</ul>
<h2>Should You Enable Them?</h2>
<p>My recommendation:</p>
<ol>
<li><p><strong>Enable</strong> <code>Style/SelectByKind</code> - because it encodes idiomatic Ruby and keeps intent explicit.</p>
</li>
<li><p><strong>Enable</strong> <code>Style/SelectByRange</code> - because it keeps filtering style consistent across regex, class, and range patterns.</p>
</li>
<li><p><strong>Enable</strong> <code>Style/PartitionInsteadOfDoubleSelect</code> - because this one is both cleaner and genuinely more efficient.</p>
</li>
</ol>
<p>Since these cops were added in <code>v1.85.0</code>, they may still be in <em>pending</em> status and not enabled by default. Here is the <code>.rubocop.yml</code> snippet to enable them explicitly:</p>
<pre><code class="language-yaml">Style/SelectByKind:
  Enabled: true

Style/SelectByRange:
  Enabled: true

Style/PartitionInsteadOfDoubleSelect:
  Enabled: true
</code></pre>
<p>If your team is not used to <a href="https://docs.ruby-lang.org/en/master/Enumerable.html#method-i-grep"><code>grep</code></a> / <a href="https://docs.ruby-lang.org/en/master/Enumerable.html#method-i-grep_v"><code>grep_v</code></a>, there will be a short learning curve. I still think it is worth it.</p>
<p>I might have missed edge cases in unusual enumerables. The performance gains are small in isolation so I think performance should not be the main metric why you should consider these cops. If you saw different benchmark behavior in your app, I would genuinely like to hear it.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://github.com/rubocop/rubocop/pull/14808">PR #14808 - Add new <code>Style/SelectByKind</code> cop</a></p>
</li>
<li><p><a href="https://github.com/rubocop/rubocop/pull/14810">PR #14810 - Add new <code>Style/SelectByRange</code> cop</a></p>
</li>
<li><p><a href="https://github.com/rubocop/rubocop/pull/14923">PR #14923 - Add new <code>Style/PartitionInsteadOfDoubleSelect</code> cop</a></p>
</li>
<li><p><a href="https://github.com/rubocop/rubocop/releases/tag/v1.85.0">RuboCop 1.85.0 release notes</a></p>
</li>
<li><p><a href="https://github.com/evanphx/benchmark-ips">benchmark-ips</a></p>
</li>
<li><p><a href="https://ruby-doc.org/core/Enumerable.html#method-i-grep">Ruby docs - <code>Enumerable#grep</code></a></p>
</li>
<li><p><a href="https://ruby-doc.org/core/Enumerable.html#method-i-grep_v">Ruby docs - <code>Enumerable#grep_v</code></a></p>
</li>
<li><p><a href="https://ruby-doc.org/core/Enumerable.html#method-i-partition">Ruby docs - <code>Enumerable#partition</code></a></p>
</li>
<li><p><a href="https://bugs.ruby-lang.org/issues/14197">Ruby issue #14197 (<code>select</code> with pattern argument proposal)</a></p>
</li>
<li><p><a href="https://bugs.ruby-lang.org/issues/11049">Ruby issue #11049 (<code>grep_v</code> addition)</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to Add Rubocop MCP to Claude Code and OpenCode]]></title><description><![CDATA[I have been writing recently about Ruby tooling in AI coding editors. I covered how to enable Ruby LSP in Claude Code and how OpenCode automatically installs rubocop as a language server. Today I want]]></description><link>https://allaboutcoding.ghinda.com/how-to-add-rubocop-mcp-to-claude-code-and-opencode</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/how-to-add-rubocop-mcp-to-claude-code-and-opencode</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Rails]]></category><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[mcp]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Fri, 13 Mar 2026 05:36:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fbe0c248caf5b2fd777f7f8/8f73ad89-71a6-4baf-bb19-e568192efc2a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have been writing recently about Ruby tooling in AI coding editors. I covered <a href="https://allaboutcoding.ghinda.com/configure-claude-code-with-ruby-lsp">how to enable Ruby LSP in Claude Code</a> and <a href="https://allaboutcoding.ghinda.com/opencode-uses-rubocop-as-the-ruby-language-server">how OpenCode automatically installs rubocop as a language server</a>. Today I want to share something that takes this a step further: RuboCop now ships with its own MCP server.</p>
<p>This means you can give Claude Code or OpenCode direct access to RuboCop as a structured tool, not just as a background linter.</p>
<h2>What Is RuboCop MCP</h2>
<p><a href="https://modelcontextprotocol.io">MCP</a> stands for Model Context Protocol. It is an open standard for connecting AI applications to external systems. When RuboCop runs as an MCP server, the LLM can call it directly as a tool instead of running it as a command-line process and parsing its output.</p>
<p>RuboCop 1.85.0 (released 2026-02-26) added experimental MCP support via <a href="https://github.com/rubocop/rubocop/pull/14911">PR #14911</a>. The motivation from the contributor who built it:</p>
<blockquote>
<p>"everyone is using tools like Claude Code, Codex and Cursor"</p>
</blockquote>
<p>The MCP server exposes two tools:</p>
<ul>
<li><p><code>rubocop_inspection</code> - inspect Ruby code for offenses. Accepts a <code>path</code> to check files on disk or <code>source_code</code> for inline code. Returns detected offenses as JSON.</p>
</li>
<li><p><code>rubocop_autocorrection</code> - autocorrect offenses. Same inputs as inspection, plus a <code>safety</code> boolean (defaults to <code>true</code>). When <code>true</code>, only safe corrections are applied.</p>
</li>
</ul>
<p>The server runs as a long-lived process over stdio, which is the same principle as RuboCop's Server Mode and LSP mode. The practical benefit for an LLM: instead of parsing human-readable CLI output, it gets structured JSON with location information it can act on reliably.</p>
<p>You need at least RuboCop 1.85.0 to use this. Check your version with <code>bundle exec rubocop --version</code>.</p>
<h2>How to Add Rubocop MCP for Claude Code</h2>
<p>Here is how to install Rubocop MCP for Claude Code:</p>
<ol>
<li><p>Go to the project where you want to have the MCP enabled</p>
</li>
<li><p>Execute the following from command line:</p>
</li>
</ol>
<pre><code class="language-bash">claude mcp add rubocop -- bundle exec rubocop --mcp
</code></pre>
<p>You can then check the configuration at <code>~/.claude.json</code> (this is not <code>~/.claude/settings.json</code>) where you will see a line like this:</p>
<pre><code class="language-json">{
  "&lt;absolute path to your project&gt;": {
    "mcpServers": {
      "rubocop": {
        "type": "stdio",
        "command": "bundle",
        "args": [
          "exec",
          "rubocop",
          "--mcp"
        ],
        "env": {}
      }
    }
  }
}
</code></pre>
<p>Check that Rubocop MCP works by asking something like this to Claude:</p>
<pre><code class="language-plaintext">Tell me Rubocop offenses for test/models/user_test.rb
</code></pre>
<p>You should see something like:</p>
<img src="https://cdn.hashnode.com/uploads/covers/5fbe0c248caf5b2fd777f7f8/768e7488-3c44-48d0-ab11-d9ebd5823957.png" alt="" style="display:block;margin:0 auto" />

<h2>How to Add Rubocop MCP for OpenCode</h2>
<p>Here is how to configure Rubocop MCP for OpenCode:</p>
<ol>
<li><p>Go to the project where you want to have the MCP enabled</p>
</li>
<li><p>Create a file if you don't already have one called <code>opencode.json</code></p>
</li>
<li><p>Add the following to that file:</p>
</li>
</ol>
<pre><code class="language-json">{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "rubocop": {
      "type": "local",
      "command": ["bundle", "exec", "rubocop", "--mcp"],
      "enabled": true
    }
  }
}
</code></pre>
<p>Check that Rubocop MCP works by asking something like this to OpenCode:</p>
<pre><code class="language-plaintext">Tell me Rubocop offenses for test/models/user_test.rb
</code></pre>
<p>You should see something like:</p>
<img src="https://cdn.hashnode.com/uploads/covers/5fbe0c248caf5b2fd777f7f8/2ec7ac28-043c-46c7-b79a-0172240050aa.png" alt="" style="display:block;margin:0 auto" />

<p>It might be that I got some details wrong or that there are edge cases I did not hit. If you have feedback or corrections, I would be glad to hear them.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://docs.rubocop.org/rubocop/usage/mcp.html">RuboCop MCP documentation</a></p>
</li>
<li><p><a href="https://github.com/rubocop/rubocop/pull/14911">PR #14911 - Support built-in MCP server</a></p>
</li>
<li><p><a href="https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md#1850-2026-02-26">RuboCop 1.85.0 changelog</a></p>
</li>
<li><p><a href="https://modelcontextprotocol.io">Model Context Protocol</a></p>
</li>
<li><p><a href="https://modelcontextprotocol.io/clients">MCP clients list</a></p>
</li>
<li><p><a href="https://docs.anthropic.com/en/docs/claude-code/mcp">Claude Code MCP documentation</a></p>
</li>
<li><p><a href="https://opencode.ai/docs">OpenCode documentation</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[OpenCode Uses Rubocop as the Ruby Language Server
]]></title><description><![CDATA[Yesterday I shared about how to configure Claude Code to use Ruby LSP. Today I will talk about OpenCode.
Here the things are simple: OpenCode automatically detects what LSP you need and will install i]]></description><link>https://allaboutcoding.ghinda.com/opencode-uses-rubocop-as-the-ruby-language-server</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/opencode-uses-rubocop-as-the-ruby-language-server</guid><category><![CDATA[Ruby]]></category><category><![CDATA[opencode]]></category><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Thu, 12 Mar 2026 06:01:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fbe0c248caf5b2fd777f7f8/7975d221-f257-40ab-8dbd-3d2c85a411d3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Yesterday I shared about how to configure Claude Code to use Ruby LSP. Today I will talk about <a href="https://opencode.ai">OpenCode</a>.</p>
<p>Here the things are simple: OpenCode automatically detects what LSP you need and will install it for you.</p>
<p>In case of Ruby OpenCode labels the LSP as <em>ruby-lsp</em> in the UI, but that label turns out to be a bit misleading.</p>
<h2>What Runs Under the Hood</h2>
<p>When you open a <code>.rb</code> file in OpenCode, it automatically detects the extension, finds the nearest <code>Gemfile</code> as the project root, and sets up a language server for you.</p>
<p>The gem it installs and runs is <strong>rubocop</strong> in <code>--lsp</code> mode, not the <a href="https://shopify.github.io/ruby-lsp/">ruby-lsp</a> gem from Shopify. These are two different projects. The UI still shows <em>ruby-lsp</em> as the label, but that is kept for backward compatibility. What is actually running underneath is <code>rubocop --lsp</code>.</p>
<p>If <code>rubocop</code> is not found in your PATH, OpenCode installs it automatically into its own managed directory. It does not touch your project's <code>Gemfile</code> or your system gems. You can opt out entirely by setting <code>OPENCODE_DISABLE_LSP_DOWNLOAD=true</code>.</p>
<h2>Why Rubocop Replaced ruby-lsp</h2>
<p>This was not always the case. OpenCode originally used the ruby-lsp gem. There is a <a href="https://github.com/anomalyco/opencode/pull/4543">pull request (#4543)</a> that explains exactly why it was replaced.</p>
<p>There were two problems with ruby-lsp. The first was a flag mismatch: ruby-lsp was being invoked with <code>--stdio</code>, a flag it does not actually recognize.</p>
<p>The second problem was more fundamental: <strong>startup time</strong>. OpenCode has a 3-second timeout for LSP servers to initialize. The ruby-lsp gem was taking around 2.65 seconds to start. That is technically within the limit, but in practice it was frequently timing out before delivering any diagnostics.</p>
<p>RuboCop's LSP mode initializes in about 0.58 seconds. That is 78% faster, and reliably within the timeout. According with the PR.</p>
<h2>A Sensible Match for an AI Coding Tool</h2>
<p>The contributor who made the switch added a point I find worth thinking about.</p>
<blockquote>
<p>"rubocop doesn't include unnecessary IDE features: No autocomplete (LLM handles this), No go-to-definition (LLM provides context)"</p>
</blockquote>
<p>In a traditional editor/IDE, autocomplete and go-to-definition are essential. In an AI coding tool, the LLM already handles both. What you actually want from a language server in this context probably is linting, warnings, and syntax error detection.</p>
<h2>The Label Mismatch</h2>
<p>One thing worth knowing if you look at the OpenCode UI: the <em>ruby-lsp</em> label you see on the right side refers to the internal server ID, not the gem name. The ID was kept as <code>"ruby-lsp"</code> after the switch to avoid breaking existing configurations. So if you see that label and assume the Shopify ruby-lsp gem is running, it is not. It is rubocop.</p>
<p>It might be that I missed something or that there are edge cases I did not hit. If you have feedback or corrections, I would be glad to hear them.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://opencode.ai/docs/lsp/">OpenCode documentation - LSP</a></p>
</li>
<li><p><a href="https://github.com/anomalyco/opencode">OpenCode GitHub repository</a></p>
</li>
<li><p><a href="https://github.com/anomalyco/opencode/pull/4543">PR #4543 - fix: replace ruby-lsp with rubocop for better LSP performance</a></p>
</li>
<li><p><a href="https://docs.rubocop.org/rubocop/usage/lsp.html">RuboCop LSP support</a></p>
</li>
<li><p><a href="https://shopify.github.io/ruby-lsp/">ruby-lsp by Shopify</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to Enable Ruby LSP in Claude Code and OpenCode]]></title><description><![CDATA[RubyLSP plugin was officially added to Claude Code. See this commit
This way you can use Ruby LSP as a real tool inside Claude Code, and the setup takes just a few steps.
What is Ruby LSP and Why Does]]></description><link>https://allaboutcoding.ghinda.com/configure-claude-code-with-ruby-lsp</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/configure-claude-code-with-ruby-lsp</guid><category><![CDATA[Ruby]]></category><category><![CDATA[lsp]]></category><category><![CDATA[claude.ai]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Wed, 11 Mar 2026 10:53:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fbe0c248caf5b2fd777f7f8/2d6d331a-a8b1-409e-a999-f4e76069e913.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>RubyLSP plugin was officially added to Claude Code. See <a href="https://github.com/anthropics/claude-plugins-official/commit/80a2049c5de75236cf8ec5c6521a0c60c95ae92f">this commit</a></p>
<p>This way you can use <a href="https://shopify.github.io/ruby-lsp/">Ruby LSP</a> as a real tool inside Claude Code, and the setup takes just a few steps.</p>
<h2>What is Ruby LSP and Why Does It Matter in Claude Code</h2>
<p><a href="https://shopify.github.io/ruby-lsp/">Ruby LSP</a> is a language server for Ruby built by Shopify. It provides go-to-definition, document symbols, hover information, and diagnostics. In your editor - VS Code, Neovim, Zed - you probably already use it and do not even notice because it just works.</p>
<p>In Claude Code, having LSP support means Claude can ask for things like:</p>
<ul>
<li><p>List classes/methods/constants in a file</p>
</li>
<li><p>Jump to where a symbol is defined</p>
</li>
<li><p>Find all usages of a symbol</p>
</li>
<li><p>Get type info/docs for a symbol</p>
</li>
<li><p>Search symbols across the whole project</p>
</li>
<li><p>Find implementations of an interface</p>
</li>
<li><p>Trace call hierarchy</p>
</li>
</ul>
<p>Think about the difference: reading a Ruby file and guessing the structure vs. asking "what are all the methods in this class?" and getting back exact symbol names, kinds, and line numbers. The second is more reliable, especially in larger codebases.</p>
<h2>Step 1: Install the ruby-lsp Gem</h2>
<p>Make sure the <code>ruby-lsp</code> gem is installed in your Ruby environment for your specific Ruby version:</p>
<pre><code class="language-shell">gem install ruby-lsp
</code></pre>
<p>Verify it is available:</p>
<pre><code class="language-shell">which ruby-lsp
# =&gt; /Users/yourname/.gem/ruby/3.3.0/bin/ruby-lsp
</code></pre>
<p>If you use a version manager like <code>rbenv</code> or <code>asdf</code>, install it for the Ruby version you use in your projects.</p>
<h2>Step 2: Install the Plugin</h2>
<p>The Ruby LSP team published their plugin in the official Claude plugin directory. This means you no longer need to add a third-party marketplace first - one command is enough.</p>
<p>Run this in your terminal or directly inside Claude Code with the <code>/plugin</code> prefix:</p>
<pre><code class="language-shell"># In terminal
claude plugin install ruby-lsp@claude-plugins-official
</code></pre>
<p>Or inside Claude Code:</p>
<pre><code class="language-shell"># inside Claude Code session
/plugin install ruby-lsp@claude-plugins-official
</code></pre>
<p>The <code>@</code>claude-plugins-official suffix tells Claude Code to look up the plugin in the official directory. No need to add an extra marketplace.</p>
<h2>Step 3: Enable the LSP Tool via Environment Variable</h2>
<p>Claude Code requires an explicit opt-in to expose LSP tools. Add this to <code>~/.claude/settings.json</code> under the <code>env</code> key:</p>
<pre><code class="language-json">{
  "env": {
    "ENABLE_LSP_TOOL": "1"
  }
}
</code></pre>
<h2>The Complete settings.json</h2>
<p>Here is what the relevant parts of <code>~/.claude/settings.json</code> look like after all steps:</p>
<pre><code class="language-json">{
  "env": {
    "ENABLE_LSP_TOOL": "1"
  },
  "enabledPlugins": {
    "ruby-lsp@claude-plugins-official": true
  }
}
</code></pre>
<h2>Step 4: Restart and Verify</h2>
<p>To load the LSP plugin you need to restart Claude Code completely, not just run <code>/reload-plugins</code>.</p>
<p>Then navigate to a Ruby project and test it. Ask Claude:</p>
<blockquote>
<p>list symbols in app/models/user.rb using LSP</p>
</blockquote>
<p>Here is what you should see (of course the symbols might vary, here I have a simple User object):</p>
<pre><code class="language-markdown">⏺ LSP(operation: "documentSymbol", file: "app/models/user.rb")
  ⎿  Found 2 symbols
     Document symbols:
     User (Class) - Line 1
       has_many :sessions (Method) - Line 3

| Symbol | Kind | Line |
|---|---|---|
| User | Class | 1 |
| has_many :sessions | Method | 3 |
</code></pre>
<h2>Summary</h2>
<p>Here are the steps, in order:</p>
<ol>
<li><p>Install the gem: <code>gem install ruby-lsp</code></p>
</li>
<li><p>Install the plugin: <code>claude plugin install ruby-lsp@claude-plugins-official</code></p>
</li>
<li><p>Add <code>ENABLE_LSP_TOOL: "1"</code> to <code>~/.claude/settings.json</code> under <code>env</code></p>
</li>
<li><p>Restart Claude Code</p>
</li>
</ol>
<h2>Resources</h2>
<ul>
<li><p><a href="https://shopify.github.io/ruby-lsp/">Ruby LSP</a> - the official Ruby language server by Shopify and <a href="https://github.com/Shopify/ruby-lsp">Ruby LSP GitHub repository</a></p>
</li>
<li><p><a href="https://docs.anthropic.com/en/docs/claude-code/overview">Claude Code documentation</a></p>
</li>
<li><p><a href="https://docs.anthropic.com/en/docs/claude-code/plugins">Claude Code plugins</a></p>
</li>
<li><p><a href="https://github.com/anthropics/claude-plugins-official">Claude code official plugins Github repository</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Essential Ruby Gems for Working with Agent Skills Files]]></title><description><![CDATA[I have created a couple of gems as a foundation for creating more tools to support agents in Ruby. They are very small building blocks to be used by more complex tools.
I know there are already packages in JavaScript or other languages, but I wanted ...]]></description><link>https://allaboutcoding.ghinda.com/essential-ruby-gems-for-working-with-agent-skills-files</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/essential-ruby-gems-for-working-with-agent-skills-files</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[llm]]></category><category><![CDATA[AI]]></category><category><![CDATA[tools]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Wed, 04 Feb 2026 11:24:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770204195581/348010b2-db13-45ac-aadc-322fb845fc71.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have created a couple of gems as a foundation for creating more tools to support agents in Ruby. They are very small building blocks to be used by more complex tools.</p>
<p>I know there are already packages in JavaScript or other languages, but I wanted to have a couple of tools written in Ruby to be included in other (more complex) DX tools.</p>
<h3 id="heading-why-ruby-for-agent-tooling">Why Ruby for Agent Tooling?</h3>
<p>Ruby is both a scripting language and a web development language. This dual nature makes it a good fit suited for building developer experience tools. While most AI agent tooling exists in the JavaScript ecosystem, there is value in having native Ruby implementations that integrate naturally with Ruby workflows and Rails applications.</p>
<p>The future of building terminal user interfaces (TUI) with Ruby is getting easier. With libraries like <a target="_blank" href="https://charm-ruby.dev">Charm</a> and <a target="_blank" href="https://www.ratatui-ruby.dev">Ratatui</a> offering Ruby wrappers, the possibilities for creating sophisticated command-line tools are expanding.</p>
<h2 id="heading-three-foundation-gems">Three Foundation Gems</h2>
<p>I built three gems that work together to provide basic infrastructure for AI agent tooling:</p>
<h3 id="heading-1-agentskillsconfigurations">1. agent_skills_configurations</h3>
<p>A unified interface for discovering and accessing skill configuration paths for various AI coding agents.</p>
<p>It will give you the configuration location/folder per each installed (or not) AI coding agent. This solves the problem of knowing where different agents store their skills and configurations across different systems.</p>
<p>Key features:</p>
<ul>
<li><p>Detect installed AI coding agents</p>
</li>
<li><p>Get configuration paths for each agent</p>
</li>
<li><p>Unified interface regardless of agent type</p>
</li>
</ul>
<p>GitHub: <a target="_blank" href="https://github.com/lucianghinda/agent_skills_configurations">https://github.com/lucianghinda/agent_skills_configurations</a></p>
<pre><code class="lang-ruby"><span class="hljs-keyword">require</span> <span class="hljs-string">"agent_skills_configurations"</span>

<span class="hljs-comment"># Find a specific agent</span>
agent = AgentSkillsConfigurations.find(<span class="hljs-string">"cursor"</span>)
agent.name              <span class="hljs-comment"># =&gt; "cursor"</span>
agent.display_name      <span class="hljs-comment"># =&gt; "Cursor"</span>
agent.skills_dir        <span class="hljs-comment"># =&gt; ".cursor/skills"</span>
agent.global_skills_dir <span class="hljs-comment"># =&gt; "/Users/username/.cursor/skills"</span>

<span class="hljs-comment"># List all detected agents</span>
AgentSkillsConfigurations.detected.map(&amp;<span class="hljs-symbol">:name</span>)
<span class="hljs-comment"># =&gt; ["cursor", "claude-code", "windsurf", ...]</span>

<span class="hljs-comment"># List all configured agents (49+ supported)</span>
AgentSkillsConfigurations.all.map(&amp;<span class="hljs-symbol">:name</span>)
<span class="hljs-comment"># =&gt; ["amp", "claude-code", "cursor", "codex", "windsurf", ...]</span>
</code></pre>
<h3 id="heading-2-agentskillparser">2. agent_skill_parser</h3>
<p>A Ruby gem for parsing skill files that use YAML frontmatter and markdown body content.</p>
<p>It will parse an AgentSkill file according with specifications from agentskills dot io and return an object with those properties. This makes it easy to programmatically work with agent skills, validate them, and extract metadata.</p>
<p>Key features:</p>
<ul>
<li><p>Parse YAML frontmatter from skill files</p>
</li>
<li><p>Extract markdown body content</p>
</li>
<li><p>Return structured objects for easy manipulation</p>
</li>
<li><p>Follow <a target="_blank" href="http://agentskills.io">agentskills.io</a> specifications</p>
</li>
</ul>
<p>GitHub: <a target="_blank" href="https://github.com/lucianghinda/agent_skill_parser">https://github.com/lucianghinda/agent_skill_parser</a></p>
<p>Taking an example of a markdown for an Agent Skill like this one:</p>
<pre><code class="lang-markdown">---
name: pdf-processing
description: Extract text from PDF documents using various parsing strategies
license: Apache-2.0
compatibility: OpenAI Anthropic
metadata:
  author: Acme Inc
  version: 1.0.0
  category: document-processing
<span class="hljs-section">allowed-tools: Bash(git:<span class="hljs-emphasis">*) Read
---

## Instructions

This skill helps extract text from PDFs.

### Step 1: Download the PDF
Use the Bash tool to download PDF files from URLs.</span></span>
</code></pre>
<pre><code class="lang-ruby">skill = AgentSkillParser.parse(<span class="hljs-string">"path/to/skill.md"</span>)

skill.name          <span class="hljs-comment"># =&gt; "pdf-processing"</span>
skill.description   <span class="hljs-comment"># =&gt; "Extract text from PDF documents..."</span>
skill.body          <span class="hljs-comment"># =&gt; "## Instructions\n\nThis skill helps..."</span>

fm = skill.frontmatter

fm.name             <span class="hljs-comment"># =&gt; "pdf-processing"</span>
fm.description      <span class="hljs-comment"># =&gt; "Extract text from PDF documents..."</span>
fm.license          <span class="hljs-comment"># =&gt; "Apache-2.0"</span>
fm.compatibility    <span class="hljs-comment"># =&gt; "OpenAI Anthropic"</span>
fm.metadata         <span class="hljs-comment"># =&gt; {"author" =&gt; "Acme Inc", "version" =&gt; "1.0.0", "category" =&gt; "document-processing"}</span>
fm.allowed_tools    <span class="hljs-comment"># =&gt; [#&lt;AllowedTool name="Bash" pattern="git:*"&gt;, ...]</span>

tool = skill.frontmatter.allowed_tools.first

tool.name    <span class="hljs-comment"># =&gt; "Bash"</span>
tool.pattern <span class="hljs-comment"># =&gt; "git:*"</span>
</code></pre>
<h3 id="heading-3-agentsskillvault">3. agents_skill_vault</h3>
<p>A gem that can manage a vault (a local folder) with various skills from Github URLs.</p>
<p>Basically you can give it a list of Agent Skills (or repositories) and it will download and sync them on a local folder. This becomes the foundation for managing collections of skills, keeping them updated, and distributing them across teams.</p>
<p>Key features:</p>
<ul>
<li><p>Download skills from GitHub repositories</p>
</li>
<li><p>Manage local skill collections</p>
</li>
<li><p>Sync skills to keep them updated</p>
</li>
<li><p>Support for multiple skill sources</p>
</li>
</ul>
<p>GitHub: <a target="_blank" href="https://github.com/lucianghinda/agents_skill_vault">https://github.com/lucianghinda/agents_skill_vault</a></p>
<pre><code class="lang-ruby"><span class="hljs-keyword">require</span> <span class="hljs-string">"agents_skill_vault"</span>

<span class="hljs-comment"># Create a vault in a directory</span>
vault = AgentsSkillVault::Vault.new(<span class="hljs-symbol">storage_path:</span> <span class="hljs-string">"~/.my_vault"</span>)

<span class="hljs-comment"># Let's assume we have this Github Repo</span>

<span class="hljs-comment"># that has this organisation</span>
<span class="hljs-comment"># .</span>
<span class="hljs-comment"># ├── LICENSE</span>
<span class="hljs-comment">#└── skills</span>
<span class="hljs-comment">#    ├── commit-message</span>
<span class="hljs-comment">#    │   └── SKILL.md</span>
<span class="hljs-comment">#    ├── improving-testing</span>
<span class="hljs-comment">#    │   └── SKILL.md</span>
<span class="hljs-comment">#    └── pr-description</span>
<span class="hljs-comment">#        └── SKILL.md</span>

<span class="hljs-comment"># Add a full repository</span>
vault.add(<span class="hljs-string">"https://github.com/lucianghinda/agentic-skills"</span>)

<span class="hljs-comment"># But you can also add a specific skill</span>
<span class="hljs-comment"># Add a specific folder and even add a custom label</span>
vault.add(
  <span class="hljs-string">"https://github.com/nateberkopec/dotfiles/tree/main/files/home/.claude/skills/readme-writer"</span>,
  <span class="hljs-symbol">label:</span> <span class="hljs-string">"readme-writer"</span>
)

<span class="hljs-comment"># List all resources</span>
vault.list.each <span class="hljs-keyword">do</span> <span class="hljs-params">|resource|</span>
  puts <span class="hljs-string">"<span class="hljs-subst">#{resource.label}</span> -&gt; <span class="hljs-subst">#{resource.local_path}</span>"</span>
<span class="hljs-keyword">end</span>

<span class="hljs-comment"># This will output</span>

<span class="hljs-comment"># lucianghinda/agentic-skills/commit-message -&gt; /Users/lucian/.my_vault/lucianghinda/agentic-skills</span>
<span class="hljs-comment"># lucianghinda/agentic-skills/improving-testing -&gt; /Users/lucian/.my_vault/lucianghinda/agentic-skills</span>
<span class="hljs-comment"># lucianghinda/agentic-skills/pr-description -&gt; /Users/lucian/.my_vault/lucianghinda/agentic-skills</span>
<span class="hljs-comment"># readme-writer -&gt; /Users/Lucian/.my_vault/nateberkopec/dotfiles/files/home/.claude/skills/readme-writer</span>

<span class="hljs-comment"># Sync a specific resource</span>
result = vault.sync(<span class="hljs-string">"rails/rails"</span>)
puts <span class="hljs-string">"Synced!"</span> <span class="hljs-keyword">if</span> result.success?

<span class="hljs-comment"># Sync all resources</span>
vault.sync_all
</code></pre>
<h3 id="heading-building-with-ai-assistance">Building with AI Assistance</h3>
<p>The code created for them was generated using a combination of Claude Code + GLM via Claude Code, OpenCode and Moltbot.</p>
<p>I reviewed all the code and manually refined it until I got a version that is good enough for release. This approach allowed me to quickly scaffold the basic structure and functionality, while maintaining control over the final implementation quality.</p>
<p>AI-assisted development worked well for:</p>
<ul>
<li><p>Initial project structure and boilerplate</p>
</li>
<li><p>Basic implementation patterns</p>
</li>
<li><p>Test scaffolding</p>
</li>
<li><p>Documentation templates</p>
</li>
</ul>
<p>Human review was essential for:</p>
<ul>
<li><p>API design decisions</p>
</li>
<li><p>Edge case handling</p>
</li>
<li><p>Code organization and clarity</p>
</li>
<li><p>Ensuring consistency across all three gems</p>
</li>
</ul>
<h3 id="heading-what-you-can-build">What You Can Build</h3>
<p>I have in mind a couple of tools:</p>
<p><strong>Team skill management</strong>: Create CLI tools that distribute and sync approved agent skills across your team. No more manual copying of configuration files or sharing snippets in Slack.</p>
<p><strong>Skill validation</strong>: Build tools that parse and validate agent skills before they are used. Check for required fields, verify tool permissions, and ensure skills follow your team's standards.</p>
<p><strong>Skill discovery</strong>: Create dashboards or search tools that help developers find and understand available agent skills. Parse skills from multiple sources, display their capabilities, and show which agents support them.</p>
<p><strong>Automation workflows</strong>: Build scripts that keep agent skills synchronized across different agents (Cursor, Windsurf, etc.). Update a skill once, distribute it everywhere.</p>
<p><strong>Internal registries</strong>: Combine all three gems to build skill marketplaces or internal registries where developers can browse, test, and install skills with confidence.</p>
<h3 id="heading-looking-forward">Looking Forward</h3>
<p>I want to see more gems or libraries built with Ruby to provide a great foundation of DX. The Ruby ecosystem has an excellent developer experience. Here are some examples that maybe now they are taken for granted: IRB + Rails console, rake …</p>
<p>These three gems are a starting point. A lot of improvements can be made, but I think they can provide a solid foundation to build more gems on top of them.</p>
<p>All three are open source and available on GitHub under Apache 2.0 License. In case you wonder why Apache 2.0 Licence that is mainly as it <a target="_blank" href="https://ghinda.com/blog/opensource/2020/open-source-licenses-apache-mit-bsd.html#apache-license-20">explicitely defines the license for contributions</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Improving Git Diffs with Delta]]></title><description><![CDATA[Working with LLMs means reviewing way more diffs than before. I discovered delta a while back and this was such a huge improvement to working in a terminal and having amazing syntax highlighting for diffs.
If you spend significant time reviewing code...]]></description><link>https://allaboutcoding.ghinda.com/improving-git-diffs-with-delta</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/improving-git-diffs-with-delta</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[Git]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Fri, 16 Jan 2026 10:34:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768559571890/5d2ae8bc-6429-4087-afd6-1bba3945574e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Working with LLMs means reviewing way more diffs than before. I discovered delta a while back and this was such a huge improvement to working in a terminal and having amazing syntax highlighting for diffs.</p>
<p>If you spend significant time reviewing code changes in the terminal, delta can transform your workflow from reading simple diff output to working with syntax-highlighted, readable diffs.</p>
<h2 id="heading-installing-and-configuring-delta">Installing and Configuring Delta</h2>
<p>First, install delta from <a target="_blank" href="https://github.com/dandavison/delta">https://github.com/dandavison/delta</a></p>
<p>Then configure your <code>.gitconfig</code>:</p>
<pre><code class="lang-yaml">[<span class="hljs-string">core</span>]
    <span class="hljs-string">pager</span> <span class="hljs-string">=</span> <span class="hljs-string">delta</span>

[<span class="hljs-string">interactive</span>]
    <span class="hljs-string">diffFilter</span> <span class="hljs-string">=</span> <span class="hljs-string">delta</span> <span class="hljs-string">--color-only</span>

[<span class="hljs-string">delta</span>]
    <span class="hljs-string">navigate</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
    <span class="hljs-string">dark</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>

[<span class="hljs-string">merge</span>]
    <span class="hljs-string">conflictstyle</span> <span class="hljs-string">=</span> <span class="hljs-string">zdiff3</span>
</code></pre>
<h2 id="heading-visual-comparison">Visual Comparison</h2>
<p>The difference is immediately visible when you run <code>git diff</code>. Without delta, you get standard monochrome output that can be hard to parse, especially for longer diffs. With delta, you get:</p>
<ul>
<li><p>Syntax highlighting based on file type</p>
</li>
<li><p>Side-by-side or unified view options</p>
</li>
<li><p>Line numbers</p>
</li>
<li><p>Better visual separation between changes</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768538913060/100bf638-65da-479d-9689-2251303f86a7.png" alt="Side-by-side comparison of two git diffs: plain monochrome diff labeled &quot;without delta&quot; and colorful highlighted diff labeled &quot;with delta,&quot; showing test file changes." class="image--center mx-auto" /></p>
<h2 id="heading-configuring-lazygit-with-delta">Configuring Lazygit with Delta</h2>
<p>I use <a target="_blank" href="https://github.com/jesseduffield/lazygit">lazygit</a> for managing my git workflow. Configuring it to work with delta is simple, just add this line to your lazygit configuration:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">git:</span>
  <span class="hljs-attr">paging:</span>
    <span class="hljs-attr">pager:</span> <span class="hljs-string">delta</span> <span class="hljs-string">--dark</span> <span class="hljs-string">--paging=never</span> <span class="hljs-string">--line-numbers</span>
</code></pre>
<h2 id="heading-the-result">The Result</h2>
<p>Without delta, lazygit displays diffs in plain diff format with just a bit of highlighting. With delta configured, you get the same rich syntax highlighting and visual improvements directly in your lazygit interface.</p>
<p>Here is how lazygit looked without delta configured:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768538951035/bef75d4d-267c-4d78-9876-abb77a0f681c.png" alt="Dark-themed terminal split into git status and diff panes showing file tree, branches, commits, and Ruby test code, titled &quot;lazygit without delta.&quot;" class="image--center mx-auto" /></p>
<p>And here is how it looks like with delta configured:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768538993568/895f7493-1483-485b-a871-bcc6aa621d2b.png" alt="Terminal-style git UI showing lazygit with delta-style colored diff panels: left panes for status, branches and commits; right pane with code lines and inline red/green change highlights." class="image--center mx-auto" /></p>
<p>This makes reviewing changes faster and reduces eye strain when working through multiple diffs in a session. When you are working with LLMs and constantly reviewing generated code or refactorings, these small improvements in readability add up quickly.</p>
<hr />
<p>👉 If you like this article and want it in your inbox each week, <a target="_blank" href="https://newsletter.lucianghinda.com/"><strong>subscribe to my newsletter</strong></a>. You’ll find <strong>ideas on Ruby, software development, software testing, building products and workshops</strong>, plus notes on creativity, tech trends, and whatever else sparks my curiosity.</p>
<p>👐 Want to improve your <strong>developer testing skills</strong>? Visit <a target="_blank" href="https://goodenoughtesting.com/articles"><strong>goodenoughtesting.com/articles</strong></a> to discover resources on testing for developers.</p>
<p>👉 <a target="_blank" href="https://newsletter.shortruby.com/"><strong>Join my Short Ruby Newsletter</strong></a> for weekly Ruby updates and visit rubyandrails.info, a directory of Ruby learning content.</p>
<p>🤝 Connect with me on <a target="_blank" href="https://linkedin.com/in/lucianghinda"><strong>Linkedin</strong></a>, <a target="_blank" href="https://bsky.app/profile/lucianghinda.com"><strong>Bluesky</strong></a>, <a target="_blank" href="https://ruby.social/@lucian"><strong>Ruby.social</strong></a>, , and <a target="_blank" href="https://x.com/lucianghinda"><strong>Twitter</strong></a>, where I mostly post about Ruby and Ruby on Rails.</p>
<p>🎥 Follow <a target="_blank" href="https://www.youtube.com/@shortruby"><strong>my YouTube channel</strong></a> for short videos about Ruby and Rails.</p>
]]></content:encoded></item><item><title><![CDATA[RSpec and `let!`: Understanding the Potential Pitfalls]]></title><description><![CDATA[This is not a new topic; various resources have addressed it in different ways. Here are my reasons and explanations for why I prefer not to use 'let!' in RSpec.
When I work on a project that uses RSpec, I prefer not to use let!. Instead, I call the ...]]></description><link>https://allaboutcoding.ghinda.com/rspec-and-let-understanding-the-potential-pitfalls</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/rspec-and-let-understanding-the-potential-pitfalls</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[#rspec]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Thu, 06 Nov 2025 08:27:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1762417577219/8f86cf1b-8073-42b3-8cb8-176b6a1a84a4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is not a new topic; various resources have addressed it in different ways. Here are my reasons and explanations for why I prefer not to use 'let!' in RSpec.</p>
<p>When I work on a project that uses RSpec, I prefer not to use <code>let!</code>. Instead, I call the let variable inside the <code>before</code> block.</p>
<pre><code class="lang-ruby">RSpec.describe Thing <span class="hljs-keyword">do</span>   
 let(<span class="hljs-symbol">:precondition</span>) { create(<span class="hljs-symbol">:item</span>) }

 before  
   precondition  
 <span class="hljs-keyword">end</span>

 it <span class="hljs-string">'returns that specific value'</span> <span class="hljs-keyword">do</span>  
   <span class="hljs-comment"># do</span>
   <span class="hljs-comment"># expect  </span>
 <span class="hljs-keyword">end</span>  
<span class="hljs-keyword">end</span>
</code></pre>
<p>Taking it a step further, if you do not need to reference <code>precondition</code> in your tests, you can do this instead:</p>
<pre><code class="lang-ruby">RSpec.describe Thing <span class="hljs-keyword">do</span>   
 before  
   create(<span class="hljs-symbol">:item</span>)  
 <span class="hljs-keyword">end</span>

 it <span class="hljs-string">'returns that specific value'</span> <span class="hljs-keyword">do</span>  
   <span class="hljs-comment"># do</span>
   <span class="hljs-comment"># expect  </span>
 <span class="hljs-keyword">end</span>  
<span class="hljs-keyword">end</span>
</code></pre>
<p>First what does <code>let!</code> do? It just <a target="_blank" href="https://github.com/rspec/rspec/blob/main/rspec-core/lib/rspec/core/memoized_helpers.rb#L329">a call</a> to the normal <code>let</code> and then setting a <code>before</code> block for the same name.</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># source: https://github.com/rspec/rspec/blob/main/rspec-core/lib/rspec/core/memoized_helpers.rb#L329</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">let!</span><span class="hljs-params">(name, &amp;block)</span></span>
  let(name, &amp;block)
  before { __send_<span class="hljs-number">_</span>(name) }
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-reason-1-let-is-actually-a-precondition-for-a-test-and-it-hides-it">Reason 1: <code>let!</code> is actually a precondition for a test and it hides it</h2>
<p>In testing <code>let!</code> acts as a test precondition: the required state of the system before running a specific test. This should be visible while reading the test and using <code>!</code> for this makes it hard to spot and hard to load it in the mental context.</p>
<p>It is harder to notice a <code>let!</code> among several <code>let</code> statements when you are trying to debug than seeing a <code>before</code> block which clearly communicate intention to run some preconditions.</p>
<pre><code class="lang-ruby">RSpec.describe Thing <span class="hljs-keyword">do</span>   
 let(<span class="hljs-symbol">:account</span>) { build(<span class="hljs-symbol">:account</span>) }  
 let!(<span class="hljs-symbol">:organisation</span>) { build(<span class="hljs-symbol">:organisation</span>, <span class="hljs-symbol">account:</span> account) }

 it <span class="hljs-string">'returns that specific value that we want'</span> <span class="hljs-keyword">do</span>  
   <span class="hljs-comment"># test  </span>
 <span class="hljs-keyword">end</span>  
<span class="hljs-keyword">end</span>
</code></pre>
<p>compared with:</p>
<pre><code class="lang-ruby">RSpec.describe Thing <span class="hljs-keyword">do</span>
 let(<span class="hljs-symbol">:account</span>) { build(<span class="hljs-symbol">:account</span>) }  
 let(<span class="hljs-symbol">:organisation</span>) { build(<span class="hljs-symbol">:organisation</span>, <span class="hljs-symbol">account:</span> account) }

 before
   organisation  
 <span class="hljs-keyword">end</span>

 it <span class="hljs-string">'returns that specific value that we want'</span> <span class="hljs-keyword">do</span>  
    <span class="hljs-comment"># test </span>
 <span class="hljs-keyword">end</span>  
<span class="hljs-keyword">end</span>
</code></pre>
<p>Which version makes it clearer that the organisation is created before each test?</p>
<h2 id="heading-reason-2-it-hides-the-order-of-execution-of-preconditions">Reason 2: It hides the order of execution of preconditions</h2>
<p>Here is a comparison:</p>
<pre><code class="lang-ruby">RSpec.describe Thing <span class="hljs-keyword">do</span>   
 let!(<span class="hljs-symbol">:account_a</span>) { build(<span class="hljs-symbol">:user</span>, <span class="hljs-symbol">email:</span> email) }  
 let!(<span class="hljs-symbol">:account_b</span>) { build(<span class="hljs-symbol">:user</span>, <span class="hljs-symbol">user:</span> email2) }  
 let(<span class="hljs-symbol">:organisation</span>) { build(<span class="hljs-symbol">:organisation</span>, <span class="hljs-symbol">account:</span> account) }  
 let(<span class="hljs-symbol">:team</span>) { build(<span class="hljs-symbol">:team</span>, <span class="hljs-symbol">account:</span> organisation) }  

 it <span class="hljs-string">'returns that specific value that we want'</span> <span class="hljs-keyword">do</span>  
    <span class="hljs-comment"># test </span>
 <span class="hljs-keyword">end</span>  
<span class="hljs-keyword">end</span>
</code></pre>
<p>compared to</p>
<pre><code class="lang-ruby">RSpec.describe Thing <span class="hljs-keyword">do</span>   
 let(<span class="hljs-symbol">:account_a</span>) { build(<span class="hljs-symbol">:user</span>, <span class="hljs-symbol">email:</span> email) }  
 let(<span class="hljs-symbol">:account_b</span>) { build(<span class="hljs-symbol">:user</span>, <span class="hljs-symbol">user:</span> email2) }  
 let(<span class="hljs-symbol">:organisation</span>) { build(<span class="hljs-symbol">:organisation</span>, <span class="hljs-symbol">account:</span> account) }  
 let(<span class="hljs-symbol">:team</span>) { build(<span class="hljs-symbol">:team</span>, <span class="hljs-symbol">account:</span> organisation) }  

 before  
   account_a  
   account_b  
 <span class="hljs-keyword">end</span>

 it <span class="hljs-string">'returns that specific value that we want'</span> <span class="hljs-keyword">do</span>  
    <span class="hljs-comment"># test </span>
 <span class="hljs-keyword">end</span>  
<span class="hljs-keyword">end</span>
</code></pre>
<p>But imagine that after a series of changes there is a risk that someone might put a <code>let!</code> among other calls.</p>
<pre><code class="lang-ruby">RSpec.describe Thing <span class="hljs-keyword">do</span>   
 let!(<span class="hljs-symbol">:account_a</span>) { build(<span class="hljs-symbol">:user</span>, <span class="hljs-symbol">email:</span> email) }  
 let(<span class="hljs-symbol">:organisation</span>) { build(<span class="hljs-symbol">:organisation</span>, <span class="hljs-symbol">account:</span> account) }  
 let!(<span class="hljs-symbol">:account_b</span>) { build(<span class="hljs-symbol">:user</span>, <span class="hljs-symbol">user:</span> email2) }
 let(<span class="hljs-symbol">:team</span>) { build(<span class="hljs-symbol">:team</span>, <span class="hljs-symbol">account:</span> organisation) }  

 it <span class="hljs-string">'returns that specific value that we want'</span> <span class="hljs-keyword">do</span>  
    <span class="hljs-comment"># test </span>
 <span class="hljs-keyword">end</span>  
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-a-minitest-equivalent">A Minitest equivalent</h2>
<p>Here are three ways to write something similar in Minitest:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ThingTest</span> &lt; Minitest::Test  </span>
   <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">setup</span>  </span>
       @account_a = build(<span class="hljs-symbol">:user</span>, <span class="hljs-symbol">email:</span> email)  
       @account_b = build(<span class="hljs-symbol">:account</span>, <span class="hljs-symbol">user:</span> user)  
       @organisation =  build(<span class="hljs-symbol">:organisation</span>, <span class="hljs-symbol">account:</span> account)  
   <span class="hljs-keyword">end</span>

   <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_computed_slug_returns_with_dashes</span>  </span>
       @account_a.name = <span class="hljs-string">"My Name"</span>        

       assert_equal <span class="hljs-string">"my-name"</span>, @account_a.computed_slug  
   <span class="hljs-keyword">end</span>  
<span class="hljs-keyword">end</span>
</code></pre>
<p>Here is another approach:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ThingTest</span> &lt; Minitest::Test  </span>
   <span class="hljs-keyword">attr_accessor</span> <span class="hljs-symbol">:account_a</span>, <span class="hljs-symbol">:account_b</span>, <span class="hljs-symbol">:organisation</span>  

   <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">setup</span>  </span>
       @account_a = build(<span class="hljs-symbol">:user</span>, <span class="hljs-symbol">email:</span> email)  
       @account_b = build(<span class="hljs-symbol">:account</span>, <span class="hljs-symbol">user:</span> user)  
       @organisation =  build(<span class="hljs-symbol">:organisation</span>, <span class="hljs-symbol">account:</span> account)  
   <span class="hljs-keyword">end</span>

   <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_computed_slug_returns_with_dashes</span>  </span>
       account_a.name = <span class="hljs-string">"My Name"</span>

       assert_equal <span class="hljs-string">"my-name"</span>, account_a.computed_slug  
   <span class="hljs-keyword">end</span>  
<span class="hljs-keyword">end</span>
</code></pre>
<hr />
<p>👉 If you like this article and want it in your inbox each week, <a target="_blank" href="https://newsletter.lucianghinda.com">subscribe to my newsletter</a>. You’ll find <strong>ideas on Ruby, software development, software testing, building products and workshops</strong>, plus notes on creativity, tech trends, and whatever else sparks my curiosity.</p>
<p>👐 Want to improve your <strong>developer testing skills</strong>? Visit <a target="_blank" href="https://goodenoughtesting.com/articles">goodenoughtesting.com/articles</a> to discover resources on testing for developers.</p>
<p>👉 <a target="_blank" href="https://newsletter.shortruby.com">Join my Short Ruby Newsletter</a> for weekly Ruby updates and visit rubyandrails.info, a directory of Ruby learning content.</p>
<p>🤝 Connect with me on <a target="_blank" href="https://linkedin.com/in/lucianghinda">Linkedin</a>, <a target="_blank" href="https://bsky.app/profile/lucianghinda.com">Bluesky</a>, <a target="_blank" href="https://ruby.social/@lucian">Ruby.social</a>, , and <a target="_blank" href="https://x.com/lucianghinda">Twitter</a>, where I mostly post about Ruby and Ruby on Rails.</p>
<p>🎥 Follow <a target="_blank" href="https://www.youtube.com/@shortruby">my YouTube channel</a> for short videos about Ruby and Rails.</p>
]]></content:encoded></item><item><title><![CDATA[How to Use Pattern Matching to Locate Elements in a Hash Array]]></title><description><![CDATA[Having the following structure of a Hash that includes an Array of Hashes, for example, and you want the email of a moderator:
system = {
  users: [
    { username: 'alice', role: 'admin', email: 'alice@example.com' },
    { username: 'bob', role: 'u...]]></description><link>https://allaboutcoding.ghinda.com/how-to-use-pattern-matching-to-locate-elements-in-a-hash-array</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/how-to-use-pattern-matching-to-locate-elements-in-a-hash-array</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[patterns]]></category><category><![CDATA[matching]]></category><category><![CDATA[pattern-matching]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Thu, 16 Oct 2025 04:54:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760590403754/f53e8f0b-02b1-4df7-8c23-bba12a4d9c0f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Having the following structure of a Hash that includes an Array of Hashes, for example, and you want the email of a moderator:</p>
<pre><code class="lang-ruby">system = {
  <span class="hljs-symbol">users:</span> [
    { <span class="hljs-symbol">username:</span> <span class="hljs-string">'alice'</span>, <span class="hljs-symbol">role:</span> <span class="hljs-string">'admin'</span>, <span class="hljs-symbol">email:</span> <span class="hljs-string">'alice@example.com'</span> },
    { <span class="hljs-symbol">username:</span> <span class="hljs-string">'bob'</span>, <span class="hljs-symbol">role:</span> <span class="hljs-string">'user'</span>, <span class="hljs-symbol">email:</span> <span class="hljs-string">'bob@example.com'</span> },
    { <span class="hljs-symbol">username:</span> <span class="hljs-string">'charlie'</span>, <span class="hljs-symbol">role:</span> <span class="hljs-string">'moderator'</span>, <span class="hljs-symbol">email:</span> <span class="hljs-string">'charlie@example.com'</span> }
  ]
}

moderator = system[<span class="hljs-symbol">:users</span>].find { <span class="hljs-params">|u|</span> u[<span class="hljs-symbol">:role</span>] == <span class="hljs-string">'moderator'</span> }
email = moderator[<span class="hljs-symbol">:email</span>] 

puts email <span class="hljs-comment"># charlie<span class="hljs-doctag">@example</span>.com</span>
</code></pre>
<p>Now here is doing the same thing in Ruby using pattern matching:</p>
<pre><code class="lang-ruby">system = {
  <span class="hljs-symbol">users:</span> [
    { <span class="hljs-symbol">username:</span> <span class="hljs-string">'alice'</span>, <span class="hljs-symbol">role:</span> <span class="hljs-string">'admin'</span>, <span class="hljs-symbol">email:</span> <span class="hljs-string">'alice@example.com'</span> },
    { <span class="hljs-symbol">username:</span> <span class="hljs-string">'bob'</span>, <span class="hljs-symbol">role:</span> <span class="hljs-string">'user'</span>, <span class="hljs-symbol">email:</span> <span class="hljs-string">'bob@example.com'</span> },
    { <span class="hljs-symbol">username:</span> <span class="hljs-string">'charlie'</span>, <span class="hljs-symbol">role:</span> <span class="hljs-string">'moderator'</span>, <span class="hljs-symbol">email:</span> <span class="hljs-string">'charlie@example.com'</span> }
  ]
}

system =&gt; {<span class="hljs-symbol">users:</span> [*, { <span class="hljs-symbol">role:</span> <span class="hljs-string">'moderator'</span>, <span class="hljs-symbol">email:</span> }, *]}
puts email <span class="hljs-comment"># charlie<span class="hljs-doctag">@example</span>.com</span>
</code></pre>
<p>Let’s put the two lines one above the other:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># Using Enumerator#find</span>
moderator = system[<span class="hljs-symbol">:users</span>].find { <span class="hljs-params">|u|</span> u[<span class="hljs-symbol">:role</span>] == <span class="hljs-string">'moderator'</span> }
email = moderator[<span class="hljs-symbol">:email</span>] 

<span class="hljs-comment"># Using Pattern Matching</span>
system =&gt; {<span class="hljs-symbol">users:</span> [*, { <span class="hljs-symbol">role:</span> <span class="hljs-string">'moderator'</span>, <span class="hljs-symbol">email:</span> }, *]}
</code></pre>
<p>There is another difference between them: in case of Pattern Matching it will throw a <code>NoMatchingPatternError</code> when not found, while the two lines that use <code>Enumerator#find</code> will throw a <code>NoMethodError</code>:</p>
<pre><code class="lang-ruby">moderator = system[<span class="hljs-symbol">:users</span>].find { <span class="hljs-params">|u|</span> u[<span class="hljs-symbol">:role</span>] == <span class="hljs-string">'THIS_ROLE_DOES_NOT_EXISTS'</span> }
email = moderator[<span class="hljs-symbol">:email</span>] 
<span class="hljs-comment"># undefined method '[]' for nil (NoMethodError)</span>

system =&gt; {<span class="hljs-symbol">users:</span> [*, { <span class="hljs-symbol">role:</span> <span class="hljs-string">'THIS_ROLE_DOES_NOT_EXISTS'</span>, <span class="hljs-symbol">email:</span> }, *]}
<span class="hljs-comment"># {users: [{username: "alice", role: "admin", email: "alice<span class="hljs-doctag">@example</span>.com"}, </span>
<span class="hljs-comment"># {username: "bob", role: "user", email: "bob<span class="hljs-doctag">@example</span>.com"}, </span>
<span class="hljs-comment"># {username: "charlie", role: "moderator", email: "charlie<span class="hljs-doctag">@example</span>.com"}]}: </span>
<span class="hljs-comment"># [{username: "alice", role: "admin", email: "alice<span class="hljs-doctag">@example</span>.com"}, </span>
<span class="hljs-comment"># {username: "bob", role: "user", email: "bob<span class="hljs-doctag">@example</span>.com"}, </span>
<span class="hljs-comment"># {username: "charlie", role: "moderator", email: "charlie<span class="hljs-doctag">@example</span>.com"}] </span>
<span class="hljs-comment"># does not match to find pattern (NoMatchingPatternError)does not match </span>
<span class="hljs-comment"># to find pattern (NoMatchingPatternError)</span>
</code></pre>
<h2 id="heading-how-does-pattern-matching-work-in-this-example">How does pattern matching work in this example</h2>
<p>If <code>email</code> does <em>not exist</em> either as a variable or method, matching it will create a local variable with the value from the matched row's key.</p>
<pre><code class="lang-ruby">system = {
  <span class="hljs-symbol">users:</span> [
    { <span class="hljs-symbol">username:</span> <span class="hljs-string">'charlie'</span>, <span class="hljs-symbol">role:</span> <span class="hljs-string">'moderator'</span>, <span class="hljs-symbol">email:</span> <span class="hljs-string">'charlie@example.com'</span> }
  ]
}
system =&gt; {<span class="hljs-symbol">users:</span> [*, { <span class="hljs-symbol">role:</span> <span class="hljs-string">'moderator'</span>, <span class="hljs-symbol">email:</span> }, *]}
puts binding.local_variables
<span class="hljs-comment"># =&gt; [:system, :email]</span>
</code></pre>
<p>If a variable of method labeled <code>email</code> does exists, it will just try to match it against its value:</p>
<pre><code class="lang-ruby">system = {
  <span class="hljs-symbol">users:</span> [
    { <span class="hljs-symbol">username:</span> <span class="hljs-string">'charlie'</span>, <span class="hljs-symbol">role:</span> <span class="hljs-string">'moderator'</span>, <span class="hljs-symbol">email:</span> <span class="hljs-string">'charlie@example.com'</span> }
  ]
}

email = <span class="hljs-string">'charlie@example.com'</span>
system =&gt; {<span class="hljs-symbol">users:</span> [*, { <span class="hljs-symbol">role:</span> <span class="hljs-string">'moderator'</span>, <span class="hljs-symbol">email:</span> }, *]}
puts email <span class="hljs-comment"># charlie<span class="hljs-doctag">@example</span>.com</span>
</code></pre>
<p>If the variable already has a value, it will be overriden if the matching is succesful:</p>
<pre><code class="lang-ruby">system = {
  <span class="hljs-symbol">users:</span> [
    { <span class="hljs-symbol">username:</span> <span class="hljs-string">'charlie'</span>, <span class="hljs-symbol">role:</span> <span class="hljs-string">'moderator'</span>, <span class="hljs-symbol">email:</span> <span class="hljs-string">'charlie@example.com'</span> }
  ]
}
email = <span class="hljs-string">'bob@example.com'</span>
system =&gt; {<span class="hljs-symbol">users:</span> [*, { <span class="hljs-symbol">role:</span> <span class="hljs-string">'moderator'</span>, <span class="hljs-symbol">email:</span> }, *]}
puts email <span class="hljs-comment"># charlie<span class="hljs-doctag">@example</span>.com</span>
</code></pre>
<p>You can force if you want to search for the content of that variable with the <code>^</code> operator:</p>
<pre><code class="lang-ruby">system = {
  <span class="hljs-symbol">users:</span> [
    { <span class="hljs-symbol">username:</span> <span class="hljs-string">'charlie'</span>, <span class="hljs-symbol">role:</span> <span class="hljs-string">'moderator'</span>, <span class="hljs-symbol">email:</span> <span class="hljs-string">'charlie@example.com'</span> }
  ]
}
email = <span class="hljs-string">'bob@example.com'</span>
system =&gt; {<span class="hljs-symbol">users:</span> [*, { <span class="hljs-symbol">role:</span> <span class="hljs-string">'moderator'</span>, <span class="hljs-symbol">email:</span> ^email}, *]}
<span class="hljs-comment"># {users: [{username: "charlie", role: "moderator", email: "charlie<span class="hljs-doctag">@example</span>.com"}]}: [{username: "charlie", role: "moderator", email: "charlie<span class="hljs-doctag">@example</span>.com"}] does not match to find pattern (NoMatchingPatternError)</span>
</code></pre>
<hr />
<p>👉 If you like this article and want it in your inbox each week, <a target="_blank" href="https://newsletter.lucianghinda.com">subscribe to my newsletter</a>. You’ll find <strong>ideas on Ruby, software development, software testing, building products and workshops</strong>, plus notes on creativity, tech trends, and whatever else sparks my curiosity.</p>
<p>👐 Want to improve your <strong>developer testing skills</strong>? Visit <a target="_blank" href="https://goodenoughtesting.com/articles">goodenoughtesting.com/articles</a> to discover resources on testing for developers.</p>
<p>👉 <a target="_blank" href="https://newsletter.shortruby.com">Join my Short Ruby Newsletter</a> for weekly Ruby updates and visit rubyandrails.info, a directory of Ruby learning content.</p>
<p>🤝 Connect with me on <a target="_blank" href="https://linkedin.com/in/lucianghinda">Linkedin</a>, <a target="_blank" href="https://bsky.app/profile/lucianghinda.com">Bluesky</a>, <a target="_blank" href="https://ruby.social/@lucian">Ruby.social</a>, , and <a target="_blank" href="https://x.com/lucianghinda">Twitter</a>, where I mostly post about Ruby and Ruby on Rails.</p>
<p>🎥 Follow <a target="_blank" href="https://www.youtube.com/@shortruby">my YouTube channel</a> for short videos about Ruby and Rails.</p>
]]></content:encoded></item><item><title><![CDATA[Ruby on Rails: Loading Locales with Yes, No, On, and Off]]></title><description><![CDATA[Ruby on Rails uses the gem psych to load the YAML files for locales.
If you have something like this in your en.yml file:
en:
  terms:
    yes: Yes
    no: No
    switch_on: On
    switch_off: Off
    accept: true
    reject: false

And then you will...]]></description><link>https://allaboutcoding.ghinda.com/ruby-on-rails-loading-locales-with-yes-no-on-and-off</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/ruby-on-rails-loading-locales-with-yes-no-on-and-off</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[gems]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Wed, 15 Oct 2025 04:36:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760502950488/61a08276-fb05-418b-af46-24907ce52018.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Ruby on Rails uses the gem <code>psych</code> to load the YAML files for locales.</p>
<p>If you have something like this in your <code>en.yml</code> file:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">en:</span>
  <span class="hljs-attr">terms:</span>
    <span class="hljs-attr">yes:</span> <span class="hljs-literal">Yes</span>
    <span class="hljs-attr">no:</span> <span class="hljs-literal">No</span>
    <span class="hljs-attr">switch_on:</span> <span class="hljs-string">On</span>
    <span class="hljs-attr">switch_off:</span> <span class="hljs-string">Off</span>
    <span class="hljs-attr">accept:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">reject:</span> <span class="hljs-literal">false</span>
</code></pre>
<p>And then you will try to load them via Rails console like this:</p>
<pre><code class="lang-ruby">I18n.locale = <span class="hljs-symbol">:en</span> <span class="hljs-comment"># make sure you have set a default locale</span>
I18n.backend.send(<span class="hljs-symbol">:translations</span>)[<span class="hljs-symbol">:en</span>][<span class="hljs-symbol">:terms</span>]
<span class="hljs-comment"># =&gt; {true =&gt; true, false =&gt; false, switch_on: true, switch_off: false, accept: true, reject: false}</span>
</code></pre>
<p>Notice there that there is no key <code>yes</code>, <code>no</code> and more so all values <code>Yes</code>, <code>No</code>, <code>On</code> <code>Off</code>, <code>true</code>, <code>false</code> were converted to TrueClass or FalseClass in Ruby.</p>
<p>That for me was quite interesting so I dig deeper to understand why this is happening.</p>
<p>Ruby on Rails loads locales using the gem <code>i18n</code> for locales. And that gem is using <code>psych</code> or <code>yaml</code> (which is aliased from <code>psych</code> - see <a target="_blank" href="https://github.com/ruby/yaml">https://github.com/ruby/yaml</a>)</p>
<h2 id="heading-processing-yes-yes-no-no-on-on-off-off">Processing YES, yes, NO, no, on, ON, off, OFF …</h2>
<p>Looking at <a target="_blank" href="https://github.com/ruby/psych"><code>psych</code></a> here are some tests that we can run:</p>
<pre><code class="lang-ruby">assert_equal <span class="hljs-literal">true</span>, Psych.safe_load(<span class="hljs-string">"--- YES"</span>)
assert_equal <span class="hljs-literal">true</span>, Psych.safe_load(<span class="hljs-string">"--- Yes"</span>)
assert_equal <span class="hljs-literal">true</span>, Psych.safe_load(<span class="hljs-string">"--- yes"</span>)

assert_equal <span class="hljs-literal">true</span>, Psych.safe_load(<span class="hljs-string">"--- TRUE"</span>)
assert_equal <span class="hljs-literal">true</span>, Psych.safe_load(<span class="hljs-string">"--- True"</span>)
assert_equal <span class="hljs-literal">true</span>, Psych.safe_load(<span class="hljs-string">"--- true"</span>)

assert_equal <span class="hljs-literal">true</span>, Psych.safe_load(<span class="hljs-string">"--- ON"</span>)
assert_equal <span class="hljs-literal">true</span>, Psych.safe_load(<span class="hljs-string">"--- on"</span>)
assert_equal <span class="hljs-literal">true</span>, Psych.safe_load(<span class="hljs-string">"--- On"</span>)
</code></pre>
<p>Notice that it will transform all of those values in <code>TrueClass</code> / <code>true</code> .</p>
<p>If we then run these tests it will return <code>FalseClass</code> / <code>false</code> :</p>
<pre><code class="lang-ruby">assert_equal <span class="hljs-literal">false</span>, Psych.safe_load(<span class="hljs-string">"--- NO"</span>)
assert_equal <span class="hljs-literal">false</span>, Psych.safe_load(<span class="hljs-string">"--- No"</span>)
assert_equal <span class="hljs-literal">false</span>, Psych.safe_load(<span class="hljs-string">"--- no"</span>)

assert_equal <span class="hljs-literal">false</span>, Psych.safe_load(<span class="hljs-string">"--- FALSE"</span>)
assert_equal <span class="hljs-literal">false</span>, Psych.safe_load(<span class="hljs-string">"--- False"</span>)
assert_equal <span class="hljs-literal">false</span>, Psych.safe_load(<span class="hljs-string">"--- false"</span>)

assert_equal <span class="hljs-literal">false</span>, Psych.safe_load(<span class="hljs-string">"--- OFF"</span>)
assert_equal <span class="hljs-literal">false</span>, Psych.safe_load(<span class="hljs-string">"--- Off"</span>)
assert_equal <span class="hljs-literal">false</span>, Psych.safe_load(<span class="hljs-string">"--- off"</span>)
</code></pre>
<p>Could not find the exact documentation about this but it seems like <code>psych</code> implements YAML version 1.1 that defines the following:</p>
<p><a target="_blank" href="https://yaml.org/type/bool.html"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760498156367/ed80e4a1-d4d5-4edc-a8f0-551249a50941.png" alt="Screenshot from YAML version 1.1" class="image--center mx-auto" /></a></p>
<p>Notice there that it also defines <code>Y, y, N, n</code> as booleans.</p>
<pre><code class="lang-ruby">assert_equal <span class="hljs-string">"Y"</span>, Psych.safe_load(<span class="hljs-string">"--- Y"</span>)
assert_equal <span class="hljs-string">"y"</span>, Psych.safe_load(<span class="hljs-string">"--- y"</span>)

assert_equal <span class="hljs-string">"N"</span>, Psych.safe_load(<span class="hljs-string">"--- N"</span>)
assert_equal <span class="hljs-string">"n"</span>, Psych.safe_load(<span class="hljs-string">"--- n"</span>)
</code></pre>
<p>But in our case when <code>psych</code> does not do that.</p>
<h2 id="heading-why-psych-does-not-convert-y-y-n-n-to-booleans">Why Psych does not convert <code>Y, y, N, n</code> to booleans?</h2>
<p>The difference <a target="_blank" href="https://github.com/ruby/psych/blob/master/test/psych/test_boolean.rb">is documented</a> for example in psych gem test suite for example:</p>
<pre><code class="lang-ruby">    <span class="hljs-comment"># source: https://github.com/ruby/psych/blob/master/test/psych/test_boolean.rb</span>
    <span class="hljs-comment"># YAML spec says "y" and "Y" may be used as true, but Syck treats them</span>
    <span class="hljs-comment"># as literal strings</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_y</span></span>
      assert_equal <span class="hljs-string">"y"</span>, Psych.load(<span class="hljs-string">"--- y"</span>)
      assert_equal <span class="hljs-string">"Y"</span>, Psych.load(<span class="hljs-string">"--- Y"</span>)
    <span class="hljs-keyword">end</span>

    <span class="hljs-comment"># YAML spec says "n" and "N" may be used as false, but Syck treats them</span>
    <span class="hljs-comment"># as literal strings</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_n</span></span>
      assert_equal <span class="hljs-string">"n"</span>, Psych.load(<span class="hljs-string">"--- n"</span>)
      assert_equal <span class="hljs-string">"N"</span>, Psych.load(<span class="hljs-string">"--- N"</span>)
    <span class="hljs-keyword">end</span>
</code></pre>
<p><code>syck</code> was an old parser for YAML written in C that seems to have been implemented since Ruby <code>1.9</code> (maybe even earlier) and syck implemented YAML version 1.0. See <a target="_blank" href="https://ruby-doc.org/stdlib-1.9.3/libdoc/syck/rdoc/Syck.html">https://ruby-doc.org/stdlib-1.9.3/libdoc/syck/rdoc/Syck.html</a></p>
<p>I could not find the exact specification in <a target="_blank" href="https://yaml.org/spec/1.0/index.html">YAML version 1.0</a> that defines the booleans so it could be there was a discussion about this and <a target="_blank" href="https://github.com/ruby/syck"><code>syck</code></a>, as the YAML parser implemented in C and used by Ruby, decided to adopt it.</p>
<p>Here is the <code>test_boolean.rb</code> from <code>syck</code> and notice there <code>YAML spec says "y" and "Y" may be used as true, but Syck treats them # as literal strings</code></p>
<pre><code class="lang-ruby"><span class="hljs-comment">###</span>
    <span class="hljs-comment"># YAML spec says "y" and "Y" may be used as true, but Syck treats them</span>
    <span class="hljs-comment"># as literal strings</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_y</span></span>
      assert_equal <span class="hljs-string">"y"</span>, Psych.load(<span class="hljs-string">"--- y"</span>)
      assert_equal <span class="hljs-string">"Y"</span>, Psych.load(<span class="hljs-string">"--- Y"</span>)
    <span class="hljs-keyword">end</span>

    <span class="hljs-comment">###</span>
    <span class="hljs-comment"># YAML spec says "n" and "N" may be used as false, but Syck treats them</span>
    <span class="hljs-comment"># as literal strings</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_n</span></span>
      assert_equal <span class="hljs-string">"n"</span>, Psych.load(<span class="hljs-string">"--- n"</span>)
      assert_equal <span class="hljs-string">"N"</span>, Psych.load(<span class="hljs-string">"--- N"</span>)
    <span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-how-to-protect-from-this-using-rubyschemaorg">How to protect from this using RubySchema.org</h2>
<p><a target="_blank" href="https://github.com/yippee-fun/rubyschema">RubySchema</a>:</p>
<blockquote>
<p>Ruby schema is a collection of JSON schemas for common Ruby gems. With these schemas, we can now enjoy auto-complete, validation and inline documentation right in our YAML files.</p>
</blockquote>
<p>If your code editor supports YAML Language Server (and most of them do) make sure you have it installed and then if you add the following lines at the beginning of your YAML locales file:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># yaml-language-server: $schema=https://www.rubyschema.org/i18n/locale.json</span>
<span class="hljs-string">%YAML</span> <span class="hljs-number">1.1</span>
<span class="hljs-meta">---</span>
</code></pre>
<p>Then in case you will write inside the file something like this:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># yaml-language-server: $schema=https://www.rubyschema.org/i18n/locale.json</span>
<span class="hljs-string">%YAML</span> <span class="hljs-number">1.1</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">en:</span>
  <span class="hljs-attr">yes:</span> <span class="hljs-string">"Yes"</span>
  <span class="hljs-attr">on:</span> <span class="hljs-string">"On"</span>
</code></pre>
<p>Then your editor will show you a message like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760585180174/2b5457f6-efb1-4f43-8097-bfc111ed5a43.png" alt="Screenshot of Neovim displaying an error message about using yes, no as locales key" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760585231589/23879934-6c61-4b3a-9b7d-1cc95db1d1cf.png" alt="Screenshot of Zed displaying an error mesaage about using `on` as key in locales" class="image--center mx-auto" /></p>
<hr />
<p>👉 If you like this article and want it in your inbox each week, <a target="_blank" href="https://newsletter.lucianghinda.com">subscribe to my newsletter</a>. You’ll find <strong>ideas on Ruby, software development, software testing, building products and workshops</strong>, plus notes on creativity, tech trends, and whatever else sparks my curiosity.</p>
<p>👐 Want to improve your <strong>developer testing skills</strong>? Visit <a target="_blank" href="https://goodenoughtesting.com/articles">goodenoughtesting.com/articles</a> to discover resources on testing for developers.</p>
<p>👉 <a target="_blank" href="https://newsletter.shortruby.com">Join my Short Ruby Newsletter</a> for weekly Ruby updates and visit rubyandrails.info, a directory of Ruby learning content.</p>
<p>🤝 Connect with me on <a target="_blank" href="https://linkedin.com/in/lucianghinda">Linkedin</a>, <a target="_blank" href="https://bsky.app/profile/lucianghinda.com">Bluesky</a>, <a target="_blank" href="https://ruby.social/@lucian">Ruby.social</a>, , and <a target="_blank" href="https://x.com/lucianghinda">Twitter</a>, where I mostly post about Ruby and Ruby on Rails.</p>
<p>🎥 Follow <a target="_blank" href="https://www.youtube.com/@shortruby">my YouTube channel</a> for short videos about Ruby and Rails.</p>
]]></content:encoded></item><item><title><![CDATA[Avoid Microsecond Pitfalls When Comparing Times in Tests]]></title><description><![CDATA[If microsecond precision is not required when testing Time, DateTime, or ActiveSupport::TimeWithZone, use iso8601 to assert equality between different times.   There are two ways to avoid test failures caused by execution delays:

Use time.iso8601

U...]]></description><link>https://allaboutcoding.ghinda.com/avoid-microsecond-pitfalls-when-comparing-times-in-tests</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/avoid-microsecond-pitfalls-when-comparing-times-in-tests</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Rails]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Fri, 10 Oct 2025 03:58:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760068666546/27bdcfb2-ff3c-4b44-a005-78bdb38dc7d0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If microsecond precision is not required when testing Time, DateTime, or ActiveSupport::TimeWithZone, use iso8601 to assert equality between different times.   There are two ways to avoid test failures caused by execution delays:</p>
<ol>
<li><p>Use time.iso8601</p>
</li>
<li><p>Use time.to_fs(:iso8601)</p>
</li>
</ol>
<h2 id="heading-comparing-two-datetime-values">Comparing two DateTime values</h2>
<p>For example, to compare two DateTime values maybe you can try to write it like this:</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_question_answered_at_the_same_as_survey</span></span>
   assert_equal question.answered_at, survey.answered_at
<span class="hljs-keyword">end</span>

describe <span class="hljs-string">'question#answered_at'</span>
   it <span class="hljs-string">'is the same as survey'</span> <span class="hljs-keyword">do</span> 
       expect(question.answered_at).to eq(survey.answered_at)
   <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>This approach can cause issues if there are microsecond delays when saving values to the database.</p>
<p>There are several ways to address this issue.</p>
<h3 id="heading-using-change">Using <code>.change</code></h3>
<p>One option is to use <a target="_blank" href="https://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html#method-i-change"><code>.change</code></a> to set microseconds to zero.</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_question_answered_at_the_same_as_survey</span></span>
   assert_equal question.answered_at.change(<span class="hljs-symbol">usec:</span> <span class="hljs-number">0</span>), survey.answered_at.change(<span class="hljs-symbol">usec:</span> <span class="hljs-number">0</span>)
<span class="hljs-keyword">end</span>

describe <span class="hljs-string">'question#answered_at'</span>
   it <span class="hljs-string">'is the same as survey'</span> <span class="hljs-keyword">do</span> 
       expect(question.answered_at.change(<span class="hljs-symbol">usec:</span> <span class="hljs-number">0</span>)).to eq(survey.answered_at.change(<span class="hljs-symbol">usec:</span> <span class="hljs-number">0</span>))
   <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-using-toi">Using <code>.to_i</code></h3>
<p>Alternatively, you can use the <a target="_blank" href="https://docs.ruby-lang.org/en/master/Time.html#method-i-to_i"><code>Time#to_i</code></a> method, which truncates subseconds:</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_question_answered_at_the_same_as_survey</span></span>
   assert_equal question.answered_at.to_i, survey.answered_at.to_i
<span class="hljs-keyword">end</span>

describe <span class="hljs-string">'question#answered_at'</span>
   it <span class="hljs-string">'is the same as survey'</span> <span class="hljs-keyword">do</span> 
       expect(question.answered_at.to_i).to eq(survey.answered_at.to_i)
   <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-using-round">Using <code>.round</code></h3>
<p>You can use from Ruby the <a target="_blank" href="https://docs.ruby-lang.org/en/master/Time.html#method-i-round"><code>Time#round</code></a> where you can specific how to round the seconds value and doing <code>Time#round</code> is equivalent to <code>Time#round(0)</code> that means without milliseconds:</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_answer_filled_at_the_same_as_survey</span>  </span>
  assert_equal(
    question.answered_at.round, 
    survey.answered_at.round
  )
<span class="hljs-keyword">end</span>

describe <span class="hljs-string">'question#answered_at'</span> <span class="hljs-keyword">do</span>
  it <span class="hljs-string">'is the same as survey'</span> <span class="hljs-keyword">do</span>   
    expect(
      question.answered_at.round
    ).to eq(survey.answered_at.round
  <span class="hljs-keyword">end</span>  
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-another-option-only-for-rspec-will-be-bewithin"><strong>Another option only for RSpec will be</strong> <code>be_within</code></h3>
<p>In RSpec there is a specific helper called <a target="_blank" href="https://rspec.info/features/3-13/rspec-expectations/built-in-matchers/be-within/"><code>be_within</code></a> which will convert Time to float and then check if the value is within a delta:</p>
<pre><code class="lang-ruby">describe <span class="hljs-string">'question#answered_at'</span> <span class="hljs-keyword">do</span>
  it <span class="hljs-string">'is the same as survey'</span> <span class="hljs-keyword">do</span>   
    expect(question.answered_at.round).to be_within(<span class="hljs-number">1</span>).of(survey.answered_at)
  <span class="hljs-keyword">end</span>  
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-in-minitest-there-is-assertindelta">In Minitest there is <code>assert_in_delta</code></h3>
<p>A similar helper exists in Minitest called <a target="_blank" href="https://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_in_delta"><code>assert_in_delta</code></a> that also transforms to float and then compare it if it is withing a delta:</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_answer_filled_at_the_same_as_survey</span>  </span>
  assert_in_delta(
    question.answered_at,
    survey.answered_at,
    <span class="hljs-number">1.0</span>
  )
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-using-iso8601">Using <code>.iso8601</code></h3>
<p>I recommend using <a target="_blank" href="https://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html#method-i-iso8601">iso8601</a> for this comparison:</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_question_answered_at_the_same_as_survey</span></span>
   assert_equal question.answered_at.iso8601, survey.answered_at.iso8601
<span class="hljs-keyword">end</span>

describe <span class="hljs-string">'question#answered_at'</span>
   it <span class="hljs-string">'is the same as survey'</span> <span class="hljs-keyword">do</span> 
       expect(question.answered_at.iso8601).to eq(survey.answered_at.iso8601)
   <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>In Rails, you can also use to_fs(:iso8601):</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_question_answered_at_the_same_as_survey</span></span>
   assert_equal question.answered_at.to_fs(<span class="hljs-symbol">:iso8601</span>), survey.answered_at.to_fs(<span class="hljs-symbol">:iso8601</span>)
<span class="hljs-keyword">end</span>

describe <span class="hljs-string">'question#answered_at'</span>
   it <span class="hljs-string">'is the same as survey'</span> <span class="hljs-keyword">do</span> 
       expect(question.answered_at.to_fs(<span class="hljs-symbol">:iso8601</span>)).to eq(survey.answered_at.to_fs(<span class="hljs-symbol">:iso8601</span>))
   <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-why-use-iso8601-or-tofsiso8601">Why use .iso8601 or .to_fs(:iso8601)</h2>
<p>First, ISO8601 is a widely adopted standard, so its behavior is consistent across Ruby and Rails implementations.</p>
<p>Second, using time.iso8601 is more descriptive than time.change(usec: 0), making the comparison method clearer and supporting consistent conventions in the codebase.</p>
<p>Third, this approach allows you to compare with fixed time values as well:</p>
<pre><code class="lang-ruby">
assert_equal question.answered_at.iso8601, <span class="hljs-string">"2025-10-07T13:35:25+03:00"</span>
expect(question.answered_at.iso8601).to eq <span class="hljs-string">"2025-10-07T13:35:25+03:00"</span>
</code></pre>
<p>I know that someone might say well to understand what iso8601 does tyou have to know about it. But here I think it is a normal expectation for an web engineer to know about ISO8601 standard when working with time.</p>
<p>Of course you should choose the one that works best for you.</p>
<hr />
<p>👉 If you like this article and want it in your inbox each week, <a target="_blank" href="https://newsletter.lucianghinda.com">subscribe to my newsletter</a>. You’ll find <strong>ideas on Ruby, software development, software testing, building products and workshops</strong>, plus notes on creativity, tech trends, and whatever else sparks my curiosity.</p>
<p>👐 Want to improve your <strong>developer testing skills</strong>? Visit <a target="_blank" href="https://goodenoughtesting.com/articles">goodenoughtesting.com/articles</a> to discover resources on testing for developers.</p>
<p>👉 <a target="_blank" href="https://newsletter.shortruby.com">Join my Short Ruby Newsletter</a> for weekly Ruby updates and visit rubyandrails.info, a directory of Ruby learning content.</p>
<p>🤝 Connect with me on <a target="_blank" href="https://linkedin.com/in/lucianghinda">Linkedin</a>, <a target="_blank" href="https://bsky.app/profile/lucianghinda.com">Bluesky</a>, <a target="_blank" href="https://ruby.social/@lucian">Ruby.social</a>, , and <a target="_blank" href="https://x.com/lucianghinda">Twitter</a>, where I mostly post about Ruby and Ruby on Rails.</p>
<p>🎥 Follow <a target="_blank" href="https://www.youtube.com/@shortruby">my YouTube channel</a> for short videos about Ruby and Rails.</p>
]]></content:encoded></item><item><title><![CDATA[Prefer getter methods over instance variables inside Ruby objects]]></title><description><![CDATA[I prefer to use getter methods instead of instance variables inside Ruby classes. I will compare getters to instance variables, focusing mainly on attr_reader.
Note: The code in this article was tested on Ruby 3.4.1
Defining getters to access instanc...]]></description><link>https://allaboutcoding.ghinda.com/prefer-getter-methods-over-instance-variables-inside-ruby-objects</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/prefer-getter-methods-over-instance-variables-inside-ruby-objects</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[properties]]></category><category><![CDATA[design patterns]]></category><category><![CDATA[design principles]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Fri, 25 Apr 2025 04:39:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745555841619/38d5afab-f529-4010-a03e-f4c5d2e31b06.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I prefer to use getter methods instead of instance variables inside Ruby classes. I will compare getters to instance variables, focusing mainly on <code>attr_reader</code>.</p>
<p><em>Note:</em> The code in this article was tested on <strong>Ruby 3.4.1</strong></p>
<h2 id="heading-defining-getters-to-access-instance-variables">Defining getters to access instance variables</h2>
<p>In most codebases that I saw, the primary use of an <code>attr_reader</code> is to provide public access to an instance variable by defining an <code>attr_reader</code> like this:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Link</span></span>
  <span class="hljs-keyword">attr_reader</span> <span class="hljs-symbol">:url</span> 

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(url)</span></span>
    @url = url
  <span class="hljs-keyword">end</span>  
<span class="hljs-keyword">end</span>
</code></pre>
<p>Funny thing you can define getters using string if you want to and Ruby will convert it to symbol and define the same method:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Link</span></span>
  <span class="hljs-keyword">attr_reader</span> <span class="hljs-string">'url'</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(url)</span></span>
    @url = url
  <span class="hljs-keyword">end</span>  
<span class="hljs-keyword">end</span>

link = Link.new(<span class="hljs-string">"https://shortruby.com"</span>)
puts link.methods.<span class="hljs-keyword">include</span>?(<span class="hljs-symbol">:url</span>) <span class="hljs-comment"># true</span>
</code></pre>
<p>You can also use <code>attr</code> to define a getter:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Link</span></span>
  attr <span class="hljs-symbol">:url</span> 

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(url)</span></span>
    @url = url
  <span class="hljs-keyword">end</span>  
<span class="hljs-keyword">end</span>
link = Link.new(<span class="hljs-string">"https://shortruby.com"</span>)
puts link.methods.<span class="hljs-keyword">include</span>?(<span class="hljs-symbol">:url</span>) <span class="hljs-comment"># true</span>
puts link.methods.<span class="hljs-keyword">include</span>?(<span class="hljs-symbol">:url=</span>) <span class="hljs-comment"># false</span>
</code></pre>
<p>There was also the option to use <code>attr :url, true</code> to define a setter but that is deprecated. Also doing <code>attr :url, false</code> is deprecated. So only <code>attr :url</code> remains which is equivalent to <code>attr_reader :url</code></p>
<p><strong>I think using</strong> <code>attr_reader</code> <strong>is better because the name of the method describes what is does.</strong></p>
<h3 id="heading-you-can-define-a-private-getter">You can define a private getter</h3>
<p>If you want to you can define a private getter like this:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Link</span></span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(url)</span></span>
    @url = url
  <span class="hljs-keyword">end</span>  

  private 

  <span class="hljs-keyword">attr_reader</span> <span class="hljs-symbol">:url</span> 
<span class="hljs-keyword">end</span>
</code></pre>
<p>And my plan in this article is to try to show why you should do this and what advantages it might bring you. I am not saying you should do this all the time, but I think it could be a case of using getters also inside the object where you are defining it.</p>
<h2 id="heading-a-short-intermission-about-private-and-public-methods">A short intermission about private and public methods</h2>
<p>When I think about a Ruby object I try to minimize the public methods that it exposes. My line of thinking is that all methods should be private unless there is a real reason to make that method public.</p>
<p>When making a method public you are signing a contract with another developer that you will keep that method the same for as long as possible (the same parameters, the same return, the same effects …). So I think the default position should be to limit your liabilities.</p>
<p>A getter is a method so when you think about defining getters you should think if you really need to expose them and make them public.</p>
<h2 id="heading-accessing-undefined-instance-variables">Accessing undefined instance variables</h2>
<p>As my main assertion here is to use getters when accesing instance variables inside the current object, I think it is important to talk a bit about instance variables and what is their value when they are not created:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Simple</span></span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">is_it_defined?</span></span>
    instance_variable_defined?(<span class="hljs-symbol">:</span>@this_does_not_exists)
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">value</span></span>
    @this_does_not_exists
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

simple = Simple.new
puts simple.is_it_defined? <span class="hljs-comment"># =&gt; false</span>
puts simple.value.inspect <span class="hljs-comment"># =&gt; nil</span>
</code></pre>
<p>We know that if an instance variable is not initialized/defined but it is directly accessed it will return <code>nil</code> but will not throw an error.</p>
<p>This opens the case of some bugs related to checking truthiness:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Simple</span></span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(payload)</span></span>
    @payload = payload
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">run</span></span>
    <span class="hljs-keyword">if</span> @paylod
      puts <span class="hljs-string">"Run with payload"</span>
    <span class="hljs-keyword">else</span> 
      puts <span class="hljs-string">"Without payload"</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Notice I wrote <code>@paylod</code> instead of <code>@payload</code> and so the result will be:</p>
<pre><code class="lang-ruby">Simple.new(<span class="hljs-string">"Something"</span>).run

<span class="hljs-comment"># Got =&gt; Without payload ❌</span>

<span class="hljs-comment"># Expected =&gt; Run with payload</span>
</code></pre>
<p>Let’s try a second example, this one a bit more complex:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">InvoiceBuilder</span></span>
  PREFIXES_FOR_EUR = [<span class="hljs-string">"INV"</span>, <span class="hljs-string">"I"</span>]

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(prefix)</span></span>
    @prefix = prefix
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">currency</span></span>
    <span class="hljs-keyword">return</span> <span class="hljs-string">"EUR"</span> <span class="hljs-keyword">if</span> PREFIXES_FOR_EUR.<span class="hljs-keyword">include</span>?(@prefix)

    <span class="hljs-string">"USD"</span> 
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>If we run two tests it will pass:</p>
<pre><code class="lang-ruby">invoice_number_builder = InvoiceBuilder.new(<span class="hljs-string">"INV"</span>)
puts invoice_number_builder.currency
<span class="hljs-comment"># =&gt; EUR </span>

invoice_number_builder = InvoiceBuilder.new(<span class="hljs-string">"CUSTOM"</span>)
puts invoice_number_builder.currency
<span class="hljs-comment"># =&gt; USD</span>
</code></pre>
<p>Again what if there will be a typo (notice the variable <code>@prefx</code> in the <code>currency</code> method:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">InvoiceBuilder</span></span>
  PREFIXES_FOR_EUR = [<span class="hljs-string">"INV"</span>, <span class="hljs-string">"I"</span>]

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(prefix)</span></span>
    @prefix = prefix
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">currency</span></span>
    <span class="hljs-keyword">return</span> <span class="hljs-string">"EUR"</span> <span class="hljs-keyword">if</span> PREFIXES_FOR_EUR.<span class="hljs-keyword">include</span>?(@prefx)

    <span class="hljs-string">"USD"</span> 
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>The same tests will fail:</p>
<pre><code class="lang-ruby">invoice_number_builder = InvoiceBuilder.new(<span class="hljs-string">"INV"</span>)
puts invoice_number_builder.currency
<span class="hljs-comment"># =&gt; USD ❌</span>

invoice_number_builder = InvoiceBuilder.new(<span class="hljs-string">"CUSTOM"</span>)
puts invoice_number_builder.currency
<span class="hljs-comment"># =&gt; USD</span>
</code></pre>
<h2 id="heading-the-case-for-using-a-getter">The case for using a getter</h2>
<p>Even if you don’t plan to expose the getter as a public interface you can define a getter and use it inside the object:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">InvoiceBuilder</span></span>
  PREFIXES_FOR_EUR = [<span class="hljs-string">"INV"</span>, <span class="hljs-string">"I"</span>]

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(prefix)</span></span>
    @prefix = prefix
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">currency</span></span>
    <span class="hljs-keyword">return</span> <span class="hljs-string">"EUR"</span> <span class="hljs-keyword">if</span> PREFIXES_FOR_EUR.<span class="hljs-keyword">include</span>?(prefix)

    <span class="hljs-string">"USD"</span>
  <span class="hljs-keyword">end</span>

  private 

  <span class="hljs-keyword">attr_reader</span> <span class="hljs-symbol">:prefix</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>And in case there is a typo like the following one:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">InvoiceBuilder</span></span>
  PREFIXES_FOR_EUR = [<span class="hljs-string">"INV"</span>, <span class="hljs-string">"I"</span>]

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(prefix)</span></span>
    @prefix = prefix
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">currency</span></span>
    <span class="hljs-keyword">return</span> <span class="hljs-string">"EUR"</span> <span class="hljs-keyword">if</span> PREFIXES_FOR_EUR.<span class="hljs-keyword">include</span>?(prefx)

    <span class="hljs-string">"USD"</span>
  <span class="hljs-keyword">end</span>

  private 

  <span class="hljs-keyword">attr_reader</span> <span class="hljs-symbol">:prefix</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>When running the following code:</p>
<pre><code class="lang-ruby">invoice_builder = InvoiceBuilder.new(<span class="hljs-string">"INV"</span>)
puts invoice_builder.currency
</code></pre>
<p>We will get a very clear error:</p>
<pre><code class="lang-ruby">test-with-attr-reader.<span class="hljs-symbol">rb:</span><span class="hljs-number">9</span><span class="hljs-symbol">:in</span> <span class="hljs-string">'InvoiceBuilder#currency'</span>: 
undefined local variable <span class="hljs-keyword">or</span> method <span class="hljs-string">'prefx'</span> <span class="hljs-keyword">for</span> an instance of InvoiceBuilder (NameError)

    <span class="hljs-keyword">return</span> <span class="hljs-string">"EUR"</span> <span class="hljs-keyword">if</span> PREFIXES_FOR_EUR.<span class="hljs-keyword">include</span>?(prefx)
                                              ^^^^^
 Did you mean?  prefix
               @prefix
        from test-with-attr-reader.<span class="hljs-symbol">rb:</span><span class="hljs-number">20</span><span class="hljs-symbol">:in</span> <span class="hljs-string">'&lt;main&gt;'</span>
</code></pre>
<p>Notice the difference:</p>
<ul>
<li><p>In the version with instance variable the execution silently returned an apparent valid response</p>
</li>
<li><p>In the version with the getter the execution will raise an exception and will also tell us exactly where the typo is located.</p>
</li>
</ul>
<h2 id="heading-protecting-against-mistakes-with-tests">Protecting against mistakes with tests</h2>
<p>You can of course protect against these kind of mistakes by writing some functional tests like the following ones (I am using here Minitest but the test framework does not make any difference).</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">InvoiceBuilderTest</span> &lt; Minitest::Test</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_currency_returns_eur_for_known_prefixes</span></span>
    prefix = <span class="hljs-string">"INV"</span>
    builder = InvoiceBuilder.new(prefix)

    assert_equal <span class="hljs-string">"EUR"</span>, builder.currency
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_currency_returns_usd_for_unknown_prefixes</span></span>
    prefix = <span class="hljs-string">"CUSTOM"</span>
    builder = InvoiceBuilder.new(prefix)

    assert_equal <span class="hljs-string">"USD"</span>, builder.currency
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>And if there is a typo in case of the version with the instrance variable if we run it we will get:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># Running:</span>
.F
Finished <span class="hljs-keyword">in</span> <span class="hljs-number">0</span>.<span class="hljs-number">00033</span>9s, <span class="hljs-number">5899.7052</span> runs/s, <span class="hljs-number">5899.7052</span> assertions/s.

  <span class="hljs-number">1</span>) <span class="hljs-symbol">Failure:</span>
InvoiceBuilderTest<span class="hljs-comment">#test_currency_returns_eur_for_known_prefixes [instance_variable_with_typo.rb:30]:</span>
<span class="hljs-symbol">Expected:</span> <span class="hljs-string">"EUR"</span>
  <span class="hljs-symbol">Actual:</span> <span class="hljs-string">"USD"</span>

<span class="hljs-number">2</span> runs, <span class="hljs-number">2</span> assertions, <span class="hljs-number">1</span> failures, <span class="hljs-number">0</span> errors, <span class="hljs-number">0</span> skips
</code></pre>
<h2 id="heading-how-to-use-this-in-rails">How to use this in Rails</h2>
<p>When talking about Rails I generally think being close to defaults is the sane choice. Still you can use this technique there too with either a getter or with a local variable instead of an instance variable.</p>
<p>Having an ERB file like this:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">if</span> @payment </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Payment exists!<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">else</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>No payment found.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>You can rewrite it like this:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">if</span> payment </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Payment exists!<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">else</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>No payment found.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>And then you can pass in the controller the <code>payment</code> using <code>locals</code>:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PaymentsController</span> &lt; ActionController::Base</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">show</span></span>
    @payment = Object.new

    render <span class="hljs-symbol">locals:</span> { <span class="hljs-symbol">payment:</span> payment }
  <span class="hljs-keyword">end</span>

  private

    <span class="hljs-keyword">attr_reader</span> <span class="hljs-symbol">:payment</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>In case there is for example a typo in ERB:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">if</span> paymnt </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Payment exists!<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">else</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>No payment found.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>When you run your tests (or the web app and access that page) you will get an error that will look like this:</p>
<pre><code class="lang-bash">ActionView::Template::Error (undefined <span class="hljs-built_in">local</span> variable or method <span class="hljs-string">'paymnt'</span> <span class="hljs-keyword">for</span> an instance of <span class="hljs-comment">#&lt;Class:0x0000000121ad6228&gt;)</span>
Caused by: NameError (undefined <span class="hljs-built_in">local</span> variable or method <span class="hljs-string">'paymnt'</span> <span class="hljs-keyword">for</span> an instance of <span class="hljs-comment">#&lt;Class:0x0000000121ad6228&gt;)</span>

Information <span class="hljs-keyword">for</span>: ActionView::Template::Error (undefined <span class="hljs-built_in">local</span> variable or method <span class="hljs-string">'paymnt'</span> <span class="hljs-keyword">for</span> an instance of <span class="hljs-comment">#&lt;Class:0x0000000121ad6228&gt;):</span>
    1: &lt;% <span class="hljs-keyword">if</span> paymnt %&gt;
    2:   &lt;p&gt;Payment exists!&lt;/p&gt;
    3: &lt;% <span class="hljs-keyword">else</span> %&gt;
    4:   &lt;p&gt;No payment found.&lt;/p&gt;

app/views/payments/show.html.erb:1
app/controllers/payments_controller.rb:5:<span class="hljs-keyword">in</span> <span class="hljs-string">'PaymentsController#show'</span>
</code></pre>
<p>Of course when doing this in a Rails controller you can get the benefit of having this nice error that tells you where you are trying to use a method that does not exists by sending local variables from the controller:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PaymentsController</span> &lt; ApplicationController</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">show</span></span>
    payment = Object.new

    render <span class="hljs-symbol">locals:</span> { <span class="hljs-symbol">payment:</span> payment }
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>This is much more simpler than writing an instance variable.</p>
<p>In the end the change that you have to explicitly call <code>render</code> with <code>locals</code> if you are not already calling it and pass it local variables instead of setting an instance variable.</p>
<pre><code class="lang-diff">def show
<span class="hljs-deletion">- @payment = Object.new</span>
<span class="hljs-addition">+ payment = Object.new</span>
<span class="hljs-addition">+ render locals: { payment: payment }</span>
end
</code></pre>
<p>One note before we move on: This is not the default Rails way so before start doing this think carefully if you plan to adopt this on your entire codebase. The</p>
<h2 id="heading-why-prefer-methods-instead-of-instance-variables">Why prefer methods instead of instance variables?</h2>
<p>Summarising the main reasons that I see for prefering method calls instead of instance variables (or in case of the Rails example local variables with render locals):</p>
<ol>
<li><p>Fails fast because it throws an exception</p>
</li>
<li><p>Ruby will point to the exact line of code that failed so identifying the root cause of the exception is easier</p>
</li>
</ol>
<p>If you're worried about performance, in most cases, the difference isn't significant. Here is <a target="_blank" href="https://gist.github.com/lucianghinda/3264a3bea89aa40613b06815552d0771">a benchmark</a> I run on my laptop (M3 Pro 32GB):</p>
<pre><code class="lang-ruby">Simple time benchmark (lower is better):
                               user     system      total        real
Instance variable <span class="hljs-symbol">access:</span>  <span class="hljs-number">0</span>.<span class="hljs-number">024115</span>   <span class="hljs-number">0</span>.<span class="hljs-number">000012</span>   <span class="hljs-number">0</span>.<span class="hljs-number">024127</span> (  <span class="hljs-number">0</span>.<span class="hljs-number">02412</span>8)
Getter <span class="hljs-symbol">access:</span>             <span class="hljs-number">0</span>.<span class="hljs-number">027</span>965   <span class="hljs-number">0</span>.<span class="hljs-number">000010</span>   <span class="hljs-number">0</span>.<span class="hljs-number">027</span>975 (  <span class="hljs-number">0</span>.<span class="hljs-number">027</span>979)

Iterations per second (higher is better):
ruby <span class="hljs-number">3.4</span>.<span class="hljs-number">1</span> (<span class="hljs-number">2024</span>-<span class="hljs-number">12</span>-<span class="hljs-number">25</span> revision <span class="hljs-number">48</span>d4efcb85) +PRISM [arm64-darwin23]
Warming up --------------------------------------
Instance variable <span class="hljs-symbol">access:</span>
                         <span class="hljs-number">3.571</span>M i/<span class="hljs-number">100</span>ms
      Getter <span class="hljs-symbol">access:</span>     <span class="hljs-number">3.215</span>M i/<span class="hljs-number">100</span>ms
Calculating -------------------------------------
Instance variable <span class="hljs-symbol">access:</span>
                         <span class="hljs-number">35.951</span>M (± <span class="hljs-number">1.0</span>%) i/s   (<span class="hljs-number">27.82</span> ns/i) -    <span class="hljs-number">182.110</span>M <span class="hljs-keyword">in</span>   <span class="hljs-number">5.065990</span>s
      Getter <span class="hljs-symbol">access:</span>     <span class="hljs-number">31.687</span>M (± <span class="hljs-number">1.1</span>%) i/s   (<span class="hljs-number">31.56</span> ns/i) -    <span class="hljs-number">160.749</span>M <span class="hljs-keyword">in</span>   <span class="hljs-number">5.073622</span>s

<span class="hljs-symbol">Comparison:</span>
Instance variable access:: <span class="hljs-number">35951125.5</span> i/s
      Getter access:: <span class="hljs-number">31687428.0</span> i/s - <span class="hljs-number">1.13</span>x  slower
</code></pre>
<h2 id="heading-you-are-already-doing-this-in-some-cases">You are already doing this in some cases</h2>
<p>I want to assert that this change is not so big. Because you are already doing this in a couple of cases:</p>
<ol>
<li><p>Predicates</p>
</li>
<li><p>Memoization and Lazy Initiatialization</p>
</li>
</ol>
<p>For example when having an instance variable that is used as a boolean we mostly write a method to make it a predicate. See in this example the <code>vat_included?</code> method:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">InvoiceBuilder</span></span>
  VAT_PERCENTAGE = <span class="hljs-number">0</span>.<span class="hljs-number">19</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(<span class="hljs-symbol">total:</span>, <span class="hljs-symbol">vat_included:</span>)</span></span>
    @total = total
    @vat_included = vat_included
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">amount</span></span>
    <span class="hljs-keyword">return</span> total <span class="hljs-keyword">unless</span> vat_included? 

    (total - (total * VAT_PERCENTAGE)).truncate(<span class="hljs-number">2</span>)
  <span class="hljs-keyword">end</span>

  private 

    <span class="hljs-keyword">attr_reader</span> <span class="hljs-symbol">:total</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">vat_included?</span></span>
      @vat_included
    <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>In case of memoization we do something like this (see the <code>products</code> method):</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Invoice</span>  </span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(<span class="hljs-symbol">product_ids:</span>)</span></span>
    @product_ids = product_ids
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">total</span>       = <span class="hljs-title">products</span>.<span class="hljs-title">sum</span> { <span class="hljs-title">_1</span>.<span class="hljs-title">price</span> } </span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">print_items</span> = <span class="hljs-title">products</span>.<span class="hljs-title">each</span> { "<span class="hljs-comment">#{it.name} | #{it.unit} | #{it.quantity}" }</span></span>

  private 

    <span class="hljs-keyword">attr_reader</span> <span class="hljs-symbol">:product_ids</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">products</span> </span>
      @products <span class="hljs-params">||</span>= Product.where(<span class="hljs-symbol">id:</span> product_ids).order(<span class="hljs-symbol">name:</span> <span class="hljs-symbol">:asc</span>)
    <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-some-conclusion">Some conclusion</h2>
<p>The typing mistakes I presented here are easy to spot because the examples are very simple. I think this is how code should look like: simple, with few lines of code if possible. If you have code like this there is no need to adopt this technique becuase you will be able to spot a mistake very easily.</p>
<p>But generally, code can be more complex, making mistakes harder to find. You'll need tests to catch cases like checking truthiness against nil values. Be aware of developer confirmation bias when writing tests, as this can make tests appear correct when they're not. Protect against these issues by setting Ruby to raise an exception. This aligns with the shift-left philosophy of catching bugs early, giving you quick and clear feedback on these typing mistakes.</p>
<hr />
<p>👉 If you like this article and want it in your inbox each week, <a target="_blank" href="https://newsletter.lucianghinda.com">subscribe to my newsletter</a>. You’ll find <strong>ideas on Ruby, software development, software testing, building products and workshops</strong>, plus notes on creativity, tech trends, and whatever else sparks my curiosity.</p>
<p>👐 Want to improve your <strong>developer testing skills</strong>? Visit <a target="_blank" href="https://goodenoughtesting.com/articles">goodenoughtesting.com/articles</a> to discover resources on testing for developers.</p>
<p>👉 <a target="_blank" href="https://newsletter.shortruby.com">Join my Short Ruby Newsletter</a> for weekly Ruby updates and visit rubyandrails.info, a directory of Ruby learning content.</p>
<p>🤝 Connect with me on <a target="_blank" href="https://linkedin.com/in/lucianghinda">Linkedin</a>, <a target="_blank" href="https://bsky.app/profile/lucianghinda.com">Bluesky</a>, <a target="_blank" href="https://ruby.social/@lucian">Ruby.social</a>, , and <a target="_blank" href="https://x.com/lucianghinda">Twitter</a>, where I mostly post about Ruby and Ruby on Rails.</p>
<p>🎥 Follow <a target="_blank" href="https://www.youtube.com/@shortruby">my YouTube channel</a> for short videos about Ruby and Rails.</p>
]]></content:encoded></item><item><title><![CDATA[More about how to create a Data class in Ruby]]></title><description><![CDATA[If you have not yet read my previous articles about Data class in Ruby, I invite you to read them first:

How to create value objects in Ruby - the idiomatic way

Example of value objects using Ruby's Data class


Here, I will talk about the two ways...]]></description><link>https://allaboutcoding.ghinda.com/more-about-how-to-create-a-data-class-in-ruby</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/more-about-how-to-create-a-data-class-in-ruby</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[coding]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Wed, 02 Apr 2025 06:55:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1743565446560/014dfc08-c712-475f-ab4d-c0b1bc0b83ab.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you have not yet read my previous articles about Data class in Ruby, I invite you to read them first:</p>
<ul>
<li><p><a target="_blank" href="https://allaboutcoding.ghinda.com/how-to-create-value-objects-in-ruby-the-idiomatic-way">How to create value objects in Ruby - the idiomatic way</a></p>
</li>
<li><p><a target="_blank" href="https://allaboutcoding.ghinda.com/example-of-value-objects-using-rubys-data-class">Example of value objects using Ruby's Data class</a></p>
</li>
</ul>
<p>Here, I will talk about the two ways you can create Data classes and compare them:</p>
<ul>
<li><p>Using the block</p>
</li>
<li><p>Using the inheritance</p>
</li>
</ul>
<h2 id="heading-how-to-create-a-data-class">How to create a Data class</h2>
<p>You can define a new Data class using the block syntax:</p>
<pre><code class="lang-ruby">Response = Data.define(<span class="hljs-symbol">:body</span>, <span class="hljs-symbol">:status</span>)
</code></pre>
<p>Looking at the <a target="_blank" href="https://docs.ruby-lang.org/en/3.4/Data.html">current Ruby docs for Ruby version 3.4</a> this seems to be the way to do it or at least the documentation is using this way of creating a new Data class. If I don’t miss anything all examples there are using this syntax.</p>
<p>You can also define a new Data class by using the inheritance</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Response</span> &lt; Data.<span class="hljs-title">define</span>(:<span class="hljs-title">body</span>, :<span class="hljs-title">status</span>)</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-the-inheritance-chain">The inheritance chain</h2>
<p>The main difference between them is that the inheritance syntax is defining an extra anonimous class.</p>
<p>Here are the ancestors for the block syntax:</p>
<pre><code class="lang-ruby">Response = Data.define(<span class="hljs-symbol">:body</span>, <span class="hljs-symbol">:status</span>) <span class="hljs-keyword">do</span>
<span class="hljs-keyword">end</span>
Response.ancestors
<span class="hljs-comment"># [</span>
<span class="hljs-comment">#   Response, </span>
<span class="hljs-comment">#   Data, </span>
<span class="hljs-comment">#   Object, </span>
<span class="hljs-comment">#   Kernel, </span>
<span class="hljs-comment">#   BasicObject</span>
<span class="hljs-comment"># ]</span>
</code></pre>
<p>The ancestors for the inheritance syntax are:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Response</span> &lt; Data.<span class="hljs-title">define</span>(:<span class="hljs-title">body</span>, :<span class="hljs-title">status</span>)</span>
<span class="hljs-keyword">end</span>
Response.ancestors
<span class="hljs-comment"># [</span>
<span class="hljs-comment">#   Response, </span>
<span class="hljs-comment">#   &lt;Class:0x000000011b98abe0&gt;, </span>
<span class="hljs-comment">#   Data, </span>
<span class="hljs-comment">#   Object, </span>
<span class="hljs-comment">#   Kernel, </span>
<span class="hljs-comment">#   BasicObject</span>
<span class="hljs-comment"># ]</span>
</code></pre>
<p>Notice there an extra <code>&lt;Class:0x000000011b98abe0&gt;</code> in the inheritance chain.</p>
<p>This is not something specific for the Data class. It works the same way for Struct too:</p>
<pre><code class="lang-ruby">Response = Struct.new(<span class="hljs-symbol">:body</span>, <span class="hljs-symbol">:status</span>)
Response.ancestors
<span class="hljs-comment"># [</span>
<span class="hljs-comment">#   Response,</span>
<span class="hljs-comment">#   Struct,</span>
<span class="hljs-comment">#   Enumerable,</span>
<span class="hljs-comment">#   Object,</span>
<span class="hljs-comment">#   Kernel,</span>
<span class="hljs-comment">#   BasicObject</span>
<span class="hljs-comment"># ]</span>
</code></pre>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Response</span> &lt; Struct.<span class="hljs-title">new</span>(:<span class="hljs-title">body</span>, :<span class="hljs-title">status</span>)</span>
<span class="hljs-keyword">end</span>
Response.ancestors
<span class="hljs-comment"># [</span>
<span class="hljs-comment">#   Response,</span>
<span class="hljs-comment">#   #&lt;Class:0x000000011d3eabe8&gt;,</span>
<span class="hljs-comment">#   Struct,</span>
<span class="hljs-comment">#   Enumerable,</span>
<span class="hljs-comment">#   Object,</span>
<span class="hljs-comment">#   Kernel,</span>
<span class="hljs-comment">#   BasicObject</span>
<span class="hljs-comment"># ]</span>
</code></pre>
<h2 id="heading-when-using-block-syntax-with-a-variable-instead-of-a-constant">When using block syntax with a variable instead of a constant</h2>
<p>If you try to assign the Data define block to a variable you will see that the name for that class is not set:</p>
<pre><code class="lang-ruby">Response = Data.define(<span class="hljs-symbol">:body</span>, <span class="hljs-symbol">:status</span>)
object = Response.new(<span class="hljs-symbol">body:</span> {}, <span class="hljs-symbol">status:</span> <span class="hljs-number">200</span>)
object.inspect
<span class="hljs-comment"># =&gt; "#&lt;data Response body={}, status=200&gt;"</span>

response = Data.define(<span class="hljs-symbol">:body</span>, <span class="hljs-symbol">:status</span>)
object = response.new(<span class="hljs-symbol">body:</span> {}, <span class="hljs-symbol">status:</span> <span class="hljs-number">200</span>)
object.inspect
<span class="hljs-comment"># =&gt; "#&lt;data body={}, status=200&gt;"</span>
</code></pre>
<p>Notice that when we inspect it when assigning to a variable it does not print the “Response” string, that is because the name is not set in case of <code>response</code>:</p>
<pre><code class="lang-ruby">Response = Data.define(<span class="hljs-symbol">:body</span>, <span class="hljs-symbol">:status</span>)

Response.name 
<span class="hljs-comment"># =&gt; "Response"</span>

response = Data.define(<span class="hljs-symbol">:body</span>, <span class="hljs-symbol">:status</span>)

response.name
<span class="hljs-comment"># =&gt; nil</span>
</code></pre>
<p>Again comparing it with Struct, the same happens for Struct:</p>
<pre><code class="lang-ruby">Response = Struct.new(<span class="hljs-symbol">:body</span>, <span class="hljs-symbol">:status</span>)

Response.name 
<span class="hljs-comment"># =&gt; "Response"</span>

response = Struct.new(<span class="hljs-symbol">:body</span>, <span class="hljs-symbol">:status</span>)

response.name
<span class="hljs-comment"># =&gt; nil</span>
</code></pre>
<h2 id="heading-nested-constants">Nested constants</h2>
<p>What happens if we try to define some constants inside.</p>
<p>If I write this code:</p>
<pre><code class="lang-ruby">Response = Data.define(<span class="hljs-symbol">:body</span>, <span class="hljs-symbol">:status</span>) <span class="hljs-keyword">do</span>
  RateLimit = Data.define(<span class="hljs-symbol">:limit</span>, <span class="hljs-symbol">:remaining</span>, <span class="hljs-symbol">:retry</span>)
<span class="hljs-keyword">end</span>
</code></pre>
<p>How do I instantiate a new object for <code>RateLimit</code>:</p>
<ul>
<li><p><code>Response::RateLimit.new …</code> ?</p>
</li>
<li><p><code>RateLimite.new</code> ?</p>
</li>
</ul>
<p>Let’s try it out:</p>
<pre><code class="lang-ruby">rate_limit = Response::RateLimit.new(<span class="hljs-symbol">limit:</span> <span class="hljs-number">10</span>, <span class="hljs-symbol">:remaining</span>: <span class="hljs-number">4</span>, <span class="hljs-symbol">retry:</span> <span class="hljs-number">1743564235</span>)
<span class="hljs-comment"># =&gt; uninitialized constant Response::RateLimit (NameError)</span>
</code></pre>
<pre><code class="lang-ruby">rate_limit = Response::RateLimit.new(<span class="hljs-symbol">limit:</span> <span class="hljs-number">10</span>, <span class="hljs-symbol">:remaining</span>: <span class="hljs-number">4</span>, <span class="hljs-symbol">retry:</span> <span class="hljs-number">1743564235</span>)
<span class="hljs-comment"># #&lt;data RateLimit limit=10, remaining=4, retry=1743564235&gt;</span>
</code></pre>
<p>This is happening because the block does not introduce a new scope and so the constrant is defined in the outher scope.</p>
<p>Here is another example that does not use the Data:</p>
<pre><code class="lang-ruby">Response = Class.new <span class="hljs-keyword">do</span>
  RateLimit = Class.new <span class="hljs-keyword">do</span>
    RETRIES = <span class="hljs-number">10</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

puts Response::RateLimit::RETRIES
<span class="hljs-comment"># =&gt; uninitialized constant Response::RateLimit (NameError)</span>

puts Response::RETRIES 
<span class="hljs-comment"># =&gt; uninitialized constant Response::RETRIES (NameError)</span>

puts RETRIES 
<span class="hljs-comment"># =&gt; 10</span>
</code></pre>
<p>This is not happening if you use the inheritance syntax for Data object:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Response</span> &lt; Data.<span class="hljs-title">define</span>(:<span class="hljs-title">body</span>, :<span class="hljs-title">status</span>)</span>
  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RateLimit</span> &lt; Data.<span class="hljs-title">define</span>(:<span class="hljs-title">limit</span>, :<span class="hljs-title">remaining</span>, :<span class="hljs-title">retry</span>)</span>
    RETRIES = <span class="hljs-number">10</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

puts Response::RateLimit::RETRIES 
<span class="hljs-comment"># =&gt; 10</span>

puts Response::RETRIES 
<span class="hljs-comment"># =&gt; uninitialized constant Response::RETRIES (NameError)</span>

puts RETRIES 
<span class="hljs-comment"># =&gt; uninitialized constant RETRIES (NameError)</span>
</code></pre>
<h2 id="heading-redefining-the-initializer">Redefining the initializer</h2>
<p>There can be multiple times when you will try to redefine the initializer and set some default values or do some other processing in there.</p>
<p>In general it looks like this:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Response</span> &lt; Data.<span class="hljs-title">define</span>(:<span class="hljs-title">body</span>, :<span class="hljs-title">status</span>)</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(<span class="hljs-symbol">body:</span> {}, <span class="hljs-symbol">status:</span> <span class="hljs-number">200</span>)</span></span>
    <span class="hljs-keyword">super</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

response = Response.new(<span class="hljs-symbol">body:</span> { <span class="hljs-symbol">message:</span> <span class="hljs-string">'Hello, world!'</span> }, <span class="hljs-symbol">status:</span> <span class="hljs-number">200</span>)
<span class="hljs-comment"># =&gt; #&lt;data Response body={message: "Hello, world!"}, status=200&gt;</span>
</code></pre>
<p>In case you will try to write the initializer with positional arguments, then you will find out it does not work:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Response</span> &lt; Data.<span class="hljs-title">define</span>(:<span class="hljs-title">body</span>, :<span class="hljs-title">status</span>)</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(body = {}, status = <span class="hljs-number">200</span>)</span></span>
    <span class="hljs-keyword">super</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

response = Response.new(<span class="hljs-symbol">body:</span> { <span class="hljs-symbol">message:</span> <span class="hljs-string">'Hello, world!'</span> }, <span class="hljs-symbol">status:</span> <span class="hljs-number">200</span>)
<span class="hljs-comment"># =&gt; in 'Data#initialize': wrong number of arguments (given 2, expected 0) (ArgumentError)</span>

response = Response.new({ <span class="hljs-symbol">message:</span> <span class="hljs-string">'Hello, world!'</span> }, <span class="hljs-number">200</span>)
<span class="hljs-comment"># =&gt; in 'Data#initialize': wrong number of arguments (given 2, expected 0) (ArgumentError)</span>
</code></pre>
<p>The <a target="_blank" href="https://docs.ruby-lang.org/en/master/Data.html">documentation in Ruby master</a> is clear about this behavior:</p>
<blockquote>
<p>Note that <code>Measure#initialize</code> always receives keyword arguments, and that mandatory arguments are checked in <code>initialize</code>, not in <code>new</code>. This can be important for redefining initialize in order to convert arguments or provide defaults.</p>
</blockquote>
<p>Just remember to always define the initializer with keyword arguments when using the Data define</p>
<h2 id="heading-which-option-to-choose-the-block-or-the-inheritance">Which option to choose the block or the inheritance?</h2>
<p>I am not sure there is a clear answer.</p>
<p>I like the simplicity of the block definition. If you don’t plan to add too many extra methods it is so cool to define it in a single line and no need for an extra <code>end</code></p>
<pre><code class="lang-ruby">Response = Data.define(<span class="hljs-symbol">:body</span>, <span class="hljs-symbol">:status</span>)
</code></pre>
<p>But once you add an extra method, like some default arguments for the initializer:</p>
<pre><code class="lang-ruby">Response = Data.define(<span class="hljs-symbol">:body</span>, <span class="hljs-symbol">:status</span>) <span class="hljs-keyword">do</span> 
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(<span class="hljs-symbol">body:</span> {}, <span class="hljs-symbol">status:</span> <span class="hljs-number">200</span>)</span></span> = <span class="hljs-keyword">super</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Then it will not be a big difference in terms of lines of code needed for defining it versus the inheritance:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Response</span> &lt; Data.<span class="hljs-title">define</span>(:<span class="hljs-title">body</span>, :<span class="hljs-title">status</span>)</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(<span class="hljs-symbol">body:</span> {}, <span class="hljs-symbol">status:</span> <span class="hljs-number">200</span>)</span></span> = <span class="hljs-keyword">super</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Still if you care about the number of objects created in your Ruby program, remember that the inheritance is creating an extra anonimous class.</p>
<hr />
<p>If you like this article:</p>
<p>👐 Interested in learning how to improve your developer testing skills? Join my live online workshop about <a target="_blank" href="http://goodenoughtesting.com/"><strong>goodenoughtesting.com</strong></a> <strong>- to learn test design techniques for writing effective tests</strong></p>
<p>👉 Join my <a target="_blank" href="https://newsletter.shortruby.com/"><strong>Short Ruby Newsletter</strong></a> for weekly Ruby updates from the community</p>
<p>🤝 Let's connect on <a target="_blank" href="https://bsky.app/profile/lucianghinda.com"><strong>Bluesky</strong></a>, <a target="_blank" href="http://ruby.social/"><strong>Ruby.social</strong></a><strong>,</strong> <a target="_blank" href="https://linkedin.com/in/lucianghinda"><strong>Linkedin</strong></a><strong>,</strong> <a target="_blank" href="https://x.com/lucianghinda"><strong>Twitter</strong></a> <strong>where I post mostly about Ruby and Ruby on Rails.</strong></p>
<p>🎥 Follow me on <a target="_blank" href="https://www.youtube.com/@shortruby"><strong>my YouTube channel</strong></a> for short videos about Ruby/Rails</p>
]]></content:encoded></item><item><title><![CDATA[Example of value objects using Ruby's Data class]]></title><description><![CDATA[Last week, I wrote an article about how to create value objects in Ruby - the idiomatic way. This week, I will share some real examples of using the data object to show some real examples.
Remove boilerplate constructor code
If you are defining class...]]></description><link>https://allaboutcoding.ghinda.com/example-of-value-objects-using-rubys-data-class</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/example-of-value-objects-using-rubys-data-class</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Rails]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[coding]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Wed, 26 Mar 2025 09:05:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742979806403/973bead1-7a96-42e2-bf87-7cddc1aba4ef.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Last week, I wrote an article about <a target="_blank" href="https://allaboutcoding.ghinda.com/how-to-create-value-objects-in-ruby-the-idiomatic-way">how to create value objects in Ruby - the idiomatic way</a>. This week, I will share some real examples of using the data object to show some real examples.</p>
<h2 id="heading-remove-boilerplate-constructor-code">Remove boilerplate constructor code</h2>
<p>If you are defining classes and expose the initializer parameters as getters and you plan to make them immutable, then I think you just found the most common case for using the Data class:</p>
<p>Instead of this:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Link</span></span>
  <span class="hljs-keyword">attr_reader</span> <span class="hljs-symbol">:url</span>, <span class="hljs-symbol">:source</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(<span class="hljs-symbol">url:</span>, <span class="hljs-symbol">source:</span>)</span></span>
    @url = url
    @source = source
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>I write this:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Link</span> &lt; Data.<span class="hljs-title">define</span>(:<span class="hljs-title">url</span>, :<span class="hljs-title">source</span>)</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>You can of course also write the simple form, but I do recommend the the previous way with inheritance (will followup in another article about this):</p>
<pre><code class="lang-ruby">Link = Data.define(<span class="hljs-symbol">:url</span>, <span class="hljs-symbol">:source</span>)
</code></pre>
<h2 id="heading-when-calling-an-external-api"><strong>When calling an external API</strong></h2>
<p>When calling an external API that returns JSON, I like to implement a method that returns a <code>Response</code> object.</p>
<p>Here I define a Response object with two computed properties:</p>
<ul>
<li><p>parsed_body</p>
</li>
<li><p>success</p>
</li>
</ul>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Response</span> &lt; Data.<span class="hljs-title">define</span>(:<span class="hljs-title">body</span>, :<span class="hljs-title">status</span>, :<span class="hljs-title">headers</span>)</span>
  HTTP_SUCCESS_STATUS_CODES = (<span class="hljs-number">200</span>..<span class="hljs-number">299</span>)

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">success?</span>    = <span class="hljs-title">HTTP_SUCCESS_STATUS_CODES</span>.<span class="hljs-title">include?</span><span class="hljs-params">(status)</span></span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">parsed_body</span> = <span class="hljs-title">JSON</span>.<span class="hljs-title">parse</span><span class="hljs-params">(body, <span class="hljs-symbol">symbolize_names:</span> <span class="hljs-literal">true</span>)</span></span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">failed?</span>     = !<span class="hljs-title">success?</span></span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Mind you that you cannot memoize using instance variables inside a Data class due to immutability. If you try something like this you will get <code>FrozenError</code></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Response</span> &lt; Data.<span class="hljs-title">define</span>(:<span class="hljs-title">body</span>, :<span class="hljs-title">status</span>, :<span class="hljs-title">headers</span>)</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">parsed_body</span> </span>
    @parsed_body <span class="hljs-params">||</span>= JSON.parse(body, <span class="hljs-symbol">symbolize_names:</span> <span class="hljs-literal">true</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

r = Response.new(<span class="hljs-symbol">body:</span> <span class="hljs-string">"{}"</span>, <span class="hljs-symbol">status:</span> <span class="hljs-number">200</span>, <span class="hljs-symbol">headers:</span> {})
r.parsed_body
<span class="hljs-comment"># can't modify frozen Response: #&lt;data Response body="{}", status=200, headers={}&gt; (FrozenError)</span>
</code></pre>
<p>A more full example might look like this using <a target="_blank" href="https://github.com/jnunemaker/httparty">httparty</a> gem</p>
<pre><code class="lang-ruby"><span class="hljs-keyword">require</span> <span class="hljs-string">'httparty'</span> 
<span class="hljs-keyword">require</span> <span class="hljs-string">'json'</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Response</span> &lt; Data.<span class="hljs-title">define</span>(:<span class="hljs-title">body</span>, :<span class="hljs-title">status</span>, :<span class="hljs-title">headers</span>)</span>
  HTTP_SUCCESS_STATUS_CODES = (<span class="hljs-number">200</span>..<span class="hljs-number">299</span>)

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">success?</span>    = <span class="hljs-title">HTTP_SUCCESS_STATUS_CODES</span>.<span class="hljs-title">include?</span><span class="hljs-params">(status)</span></span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">parsed_body</span> = <span class="hljs-title">JSON</span>.<span class="hljs-title">parse</span><span class="hljs-params">(body, <span class="hljs-symbol">symbolize_names:</span> <span class="hljs-literal">true</span>)</span></span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">failed?</span>     = !<span class="hljs-title">success?</span></span>
<span class="hljs-keyword">end</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get</span><span class="hljs-params">(url, <span class="hljs-symbol">query:</span> {})</span></span>
  response = HTTParty.get(url, query)
  Response.new(<span class="hljs-symbol">body:</span> response.body, <span class="hljs-symbol">status:</span> response.code, <span class="hljs-symbol">headers:</span> response.headers)
<span class="hljs-keyword">end</span>

response = get(
  <span class="hljs-string">'https://bsky.social/xrpc/'</span> \
  <span class="hljs-string">'com.atproto.identity.resolveHandle?handle=lucianghinda.com'</span>)
puts response.parsed_body[<span class="hljs-symbol">:did</span>] <span class="hljs-comment"># did:plc:1362asasdah213212</span>
puts response.success? <span class="hljs-comment"># true</span>
</code></pre>
<p>From this example you can for example expand it to add a RateLimit object:</p>
<pre><code class="lang-ruby"><span class="hljs-keyword">require</span> <span class="hljs-string">'httparty'</span> 
<span class="hljs-keyword">require</span> <span class="hljs-string">'json'</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RateLimit</span> &lt; Data.<span class="hljs-title">define</span>(:<span class="hljs-title">limit</span>, :<span class="hljs-title">remaining</span>, :<span class="hljs-title">reset</span>)</span>
<span class="hljs-keyword">end</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Response</span> &lt; Data.<span class="hljs-title">define</span>(:<span class="hljs-title">body</span>, :<span class="hljs-title">status</span>, :<span class="hljs-title">headers</span>, :<span class="hljs-title">rate_limit</span>)</span>
  HTTP_SUCCESS_STATUS_CODES = (<span class="hljs-number">200</span>..<span class="hljs-number">299</span>)

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">success?</span>    = <span class="hljs-title">HTTP_SUCCESS_STATUS_CODES</span>.<span class="hljs-title">include?</span><span class="hljs-params">(status)</span></span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">parsed_body</span> = <span class="hljs-title">JSON</span>.<span class="hljs-title">parse</span><span class="hljs-params">(body, <span class="hljs-symbol">symbolize_names:</span> <span class="hljs-literal">true</span>)</span></span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">failed?</span>     = !<span class="hljs-title">success?</span></span>
<span class="hljs-keyword">end</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get</span><span class="hljs-params">(url, <span class="hljs-symbol">query:</span> {})</span></span>
  response = HTTParty.get(url, query)

  rate_limit = RateLimit.new(
    <span class="hljs-symbol">limit:</span> response.headers[<span class="hljs-string">'ratelimit-limit'</span>].to_i,
    <span class="hljs-symbol">remaining:</span> response.headers[<span class="hljs-string">'ratelimit-remaining'</span>].to_i,
    <span class="hljs-symbol">reset:</span> response.headers[<span class="hljs-string">'ratelimit-reset'</span>].to_i
  )

  Response.new(
    <span class="hljs-symbol">body:</span> response.body,
    <span class="hljs-symbol">status:</span> response.code,
    <span class="hljs-symbol">headers:</span> response.headers,
    <span class="hljs-symbol">rate_limit:</span> rate_limit
  )
<span class="hljs-keyword">end</span>
</code></pre>
<p>You can even add a constructor to RateLimit for example:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RateLimit</span> &lt; Data.<span class="hljs-title">define</span>(:<span class="hljs-title">limit</span>, :<span class="hljs-title">remaining</span>, :<span class="hljs-title">reset</span>)</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">self</span>.<span class="hljs-title">from_headers</span><span class="hljs-params">(headers)</span></span>
    limit = headers[<span class="hljs-string">'ratelimit-limit'</span>].to_i
    remaining = headers[<span class="hljs-string">'ratelimit-remaining'</span>].to_i
    reset = headers[<span class="hljs-string">'ratelimit-reset'</span>].to_i

    new(<span class="hljs-symbol">limit:</span> limit, <span class="hljs-symbol">remaining:</span> remaining, <span class="hljs-symbol">reset:</span> reset)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-global-or-public-list-of-objects-from-your-domain"><strong>Global or public list of objects from your domain</strong></h2>
<p>Here is an example I found in <a target="_blank" href="https://github.com/TheOdinProject/theodinproject">TheOdinProject</a>:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># Source: https://github.com/TheOdinProject/theodinproject/app/models/flag.rb</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Flag</span> &lt; ApplicationRecord</span>
  Reason = Data.define(<span class="hljs-symbol">:name</span>, <span class="hljs-symbol">:description</span>, <span class="hljs-symbol">:value</span>)

  REASONS = [
    { <span class="hljs-symbol">name:</span> <span class="hljs-symbol">:broken</span>, <span class="hljs-symbol">description:</span> <span class="hljs-string">'Link does not work'</span>, <span class="hljs-symbol">value:</span> <span class="hljs-number">10</span> },
    { <span class="hljs-symbol">name:</span> <span class="hljs-symbol">:insecure</span>, <span class="hljs-symbol">description:</span> <span class="hljs-string">'Link is not secure or safe'</span>, <span class="hljs-symbol">value:</span> <span class="hljs-number">20</span> },
    { <span class="hljs-symbol">name:</span> <span class="hljs-symbol">:spam</span>, <span class="hljs-symbol">description:</span> <span class="hljs-string">'Spam or misleading'</span>, <span class="hljs-symbol">value:</span> <span class="hljs-number">30</span> },
    { <span class="hljs-symbol">name:</span> <span class="hljs-symbol">:inappropriate</span>, <span class="hljs-symbol">description:</span> <span class="hljs-string">'Inappropriate imagery or language'</span>, <span class="hljs-symbol">value:</span> <span class="hljs-number">40</span> },
    { <span class="hljs-symbol">name:</span> <span class="hljs-symbol">:other</span>, <span class="hljs-symbol">description:</span> <span class="hljs-string">'Other'</span>, <span class="hljs-symbol">value:</span> <span class="hljs-number">50</span> }
  ].map { <span class="hljs-params">|reason|</span> Reason.new(**reason) }
<span class="hljs-keyword">end</span>
</code></pre>
<p>Furthe on you will see they are using <code>REASONS</code> in a Rails enum:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># Source: https://github.com/TheOdinProject/theodinproject/app/models/flag.rb</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Flag</span> &lt; ApplicationRecord</span>
   enum <span class="hljs-symbol">reason:</span> REASONS.each_with_object({}) { <span class="hljs-params">|reason, hash|</span> hash[reason.name] = reason.value }
<span class="hljs-keyword">end</span>
</code></pre>
<p>And then in the view as a list of choices:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> Flag::REASONS.each <span class="hljs-keyword">do</span> <span class="hljs-params">|reason|</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"relative flex items-center"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"absolute flex h-6 items-center"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> form.radio_button(
        <span class="hljs-symbol">:reason</span>, 
        reason.name, 
        <span class="hljs-symbol">data:</span> { <span class="hljs-symbol">test_id:</span> <span class="hljs-string">"flag-reason-<span class="hljs-subst">#{reason.name}</span>"</span>}, 
        <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">h</span>-4 <span class="hljs-title">w</span>-4 <span class="hljs-title">border</span>-<span class="hljs-title">gray</span>-300 <span class="hljs-title">dark</span>:<span class="hljs-title">border</span>-<span class="hljs-title">gray</span>-500 <span class="hljs-title">dark</span>:<span class="hljs-title">bg</span>-<span class="hljs-title">gray</span>-700/50' <span class="hljs-comment"># ...</span></span>
      </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"pl-7 text-sm leading-6"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> form.label(
        <span class="hljs-symbol">:reason</span>, 
        reason.description, 
        <span class="hljs-symbol">value:</span> reason.name, 
        <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">font</span>-<span class="hljs-title">medium</span> <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-700 <span class="hljs-title">dark</span>:<span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-200 <span class="hljs-title">dark</span>:<span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-200' </span>
      </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<hr />
<p>If you like this article:</p>
<p>👐 Interested in learning how to improve your developer testing skills? Join my live online workshop about <a target="_blank" href="http://goodenoughtesting.com/"><strong>goodenoughtesting.com</strong></a> <strong>- to learn test design techniques for writing effective tests</strong></p>
<p>👉 Join my <a target="_blank" href="https://newsletter.shortruby.com/"><strong>Short Ruby Newsletter</strong></a> for weekly Ruby updates from the community</p>
<p>🤝 Let's connect on <a target="_blank" href="https://bsky.app/profile/lucianghinda.com"><strong>Bluesky</strong></a>, <a target="_blank" href="http://ruby.social/"><strong>Ruby.social</strong></a><strong>,</strong> <a target="_blank" href="https://linkedin.com/in/lucianghinda"><strong>Linkedin</strong></a><strong>,</strong> <a target="_blank" href="https://x.com/lucianghinda"><strong>Twitter</strong></a> <strong>where I post mostly about Ruby and Ruby on Rails.</strong></p>
<p>🎥 Follow me on <a target="_blank" href="https://www.youtube.com/@shortruby"><strong>my YouTube channel</strong></a> for short videos about Ruby/Rails</p>
]]></content:encoded></item><item><title><![CDATA[How to create value objects in Ruby - the idiomatic way]]></title><description><![CDATA[When writing Ruby OOP, a typical pattern might be to create an object to group multiple values together meaningfully and sometimes also add some extra methods (computed properties, predicates, representations, …) to allow the object to respond to var...]]></description><link>https://allaboutcoding.ghinda.com/how-to-create-value-objects-in-ruby-the-idiomatic-way</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/how-to-create-value-objects-in-ruby-the-idiomatic-way</guid><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Thu, 20 Mar 2025 08:36:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742459187463/2aac11ab-745f-4302-83ee-79d7949614db.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When writing Ruby OOP, a typical pattern might be to create an object to group multiple values together meaningfully and sometimes also add some extra methods (computed properties, predicates, representations, …) to allow the object to respond to various situations.</p>
<p>Here is an exploration of how to create value-alike objects in Ruby and what I think is the modern idiomatic way.</p>
<h2 id="heading-what-is-a-value-alike-object">What is a value-alike object?</h2>
<p>If you want to read an article about this concept, I recommend <a target="_blank" href="https://martinfowler.com/bliki/ValueObject.html">Value Object</a> by Martin Fowler. He explains this concept very well with examples and references. I invite you to read that article. It is not that long.</p>
<p>They are simple objects that have the following properties:</p>
<ul>
<li><p>Comparable by type and value (that means two objects having the same values as attributes and being the same class will be equal)</p>
</li>
<li><p>Immutable (once set their attributes, they should not be allowed to be changed)</p>
</li>
</ul>
<p>The concept is useful when you have to carry around multiple related values, and you need them together in most cases. When talking about Ruby and Ruby OOP, a value object is a simple immutable object that represents a concept in your domain and knows to respond to some simple messages like: what is the value of this property, what is your representation as a string, are you equal to this object in value and type and so on.</p>
<p>The simplest example is a <code>Point</code> that in 2D has <code>x</code> and <code>y</code> properties.</p>
<p>Thus, instead of carrying on a hash <code>{ x: 20, y: 30 }</code>, you carry on an object: <code>Point.new(x: 20, y:20)</code> that you can interrogate and ask questions. For example, you might want to know in which quadrant the point is, and making this an object allows you to add methods to it, like <code>quadrant</code></p>
<p>Or I can ask <code>distance_to(another_point)</code>, and it will tell me something along the lines of <code>Math.sqrt((x - other.x) ** 2 + (y - other.y) ** 2)</code></p>
<p>But most of the time, you need to ask that object <code>point.x</code> or <code>point.y</code> and have a good name for both the class and the attributes.</p>
<h2 id="heading-why-use-a-value-object">Why use a value object?</h2>
<p>You might ask yourself, why use a value object?</p>
<p>Here are some arguments for using a value object:</p>
<ol>
<li><p>Keep data together using an immutable object so you can be sure that as you pass an instance of this object through your code, nothing has changed it.</p>
</li>
<li><p>It is a group of values that should be together under a meaningful name</p>
</li>
<li><p>It is greppable, meaning you can easily find all the places in your codebase where this object is used, making it simple to refactor when needed.</p>
</li>
</ol>
<p>When comparing a value object with using a more base structure like a Hash, the advantages over Hash are:</p>
<ul>
<li><p>Naming: the structure of the values together have a name</p>
</li>
<li><p>Greppability: because now this structure has a name, it is easier to find it in the codebase</p>
</li>
<li><p>Flexibility: you can add utility methods, computed properties, or predicates</p>
</li>
</ul>
<h2 id="heading-the-idiomatic-ruby-way-since-2022">The Idiomatic Ruby way since 2022</h2>
<p>The idiomatic way to do this in Ruby should be to <a target="_blank" href="https://docs.ruby-lang.org/en/master/Data.html">use <code>Data</code> class</a>. Here is what Ruby's official documentation says about it:</p>
<blockquote>
<p>Class Data provides a convenient way to define simple classes for value-alike objects.</p>
</blockquote>
<p><a target="_blank" href="https://docs.ruby-lang.org/en/master/Data.html"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742371331135/4d55f78a-e883-4d1a-b99c-8ab65ff0589e.png" alt="Screenshot of the definition of the Data object in Ruby" class="image--center mx-auto" /></a></p>
<p>We must thank <a target="_blank" href="https://zverok.space">Victor Shepelev</a> for proposing this structure in Ruby and continuing the discussion about it until it was accepted and implemented.</p>
<h3 id="heading-how-to-define-and-create-an-instance-of-a-data-class">How to define and create an instance of a Data class</h3>
<p>Say you want to pass along an object representing an amount and save the value and the currency.</p>
<p>You can define this like:</p>
<pre><code class="lang-ruby">Price = Data.define(<span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">:currency</span>)
</code></pre>
<p>There are multiple ways to instantiate an objects from a Data class:</p>
<p><strong>Using required keyword arguments</strong></p>
<pre><code class="lang-ruby">price = Price.new(<span class="hljs-symbol">amount:</span> <span class="hljs-number">50</span>, <span class="hljs-symbol">currency:</span> <span class="hljs-string">"USD"</span>)
<span class="hljs-comment"># =&gt; #&lt;data Price amount=50, currency="USD"&gt;</span>
</code></pre>
<p><strong>Using positional arguments</strong></p>
<pre><code class="lang-ruby">price = Price.new(<span class="hljs-number">50</span>, <span class="hljs-string">"USD"</span>)
<span class="hljs-comment"># =&gt; #&lt;data Price amount=50, currency="USD"&gt;</span>
</code></pre>
<p><strong>Using alternative form construct</strong></p>
<pre><code class="lang-ruby">price = Price[<span class="hljs-string">"50"</span>, <span class="hljs-string">"USD"</span>]
<span class="hljs-comment"># =&gt; #&lt;data Price amount="50", currency="USD"&gt;</span>
</code></pre>
<p><strong>Using alternative construction hash-like form</strong></p>
<pre><code class="lang-ruby">price = Price[<span class="hljs-symbol">amount:</span> <span class="hljs-string">"50"</span>, <span class="hljs-symbol">currency:</span> <span class="hljs-string">"USD"</span>]
<span class="hljs-comment"># =&gt; #&lt;data Price amount="50", currency="USD"&gt;</span>
</code></pre>
<h3 id="heading-properties-of-the-data-object-in-ruby">Properties of the <code>Data</code> object in Ruby</h3>
<p>A Data object has two important properties in my view:</p>
<ol>
<li><strong>It is immutable</strong> so there are no setters defined and you cannot define setters</li>
</ol>
<pre><code class="lang-ruby"><span class="hljs-comment"># You cannot change the value</span>

price = Price.new(<span class="hljs-symbol">amount:</span> <span class="hljs-number">50</span>, <span class="hljs-symbol">currency:</span> <span class="hljs-string">"USD"</span>)

<span class="hljs-comment"># The following will raise an exception</span>
price.amount = <span class="hljs-number">50</span> 
<span class="hljs-comment"># =&gt; `&lt;main&gt;': undefined method `amount=' for #&lt;data Price amount=50, currency="USD"&gt; (NoMethodError)</span>
</code></pre>
<p>Even if you try to define a setter, it will not work:</p>
<pre><code class="lang-ruby">
Price = Data.define(<span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">:currency</span>) <span class="hljs-keyword">do</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">amount=</span><span class="hljs-params">(new_amount)</span></span>
      @amount = new_amount
    <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

price = Price.new(<span class="hljs-symbol">amount:</span> <span class="hljs-number">50</span>, <span class="hljs-symbol">currency:</span> <span class="hljs-string">"USD"</span>)
price.amount = <span class="hljs-number">100</span> <span class="hljs-comment"># This will return an exception</span>
<span class="hljs-comment"># =&gt; in 'Price#amount=': can't modify frozen #&lt;Class:#&lt;Price:0x00000001259166a0&gt;&gt;: #&lt;data Price amount=20, currency="USD"&gt; (FrozenError)</span>
</code></pre>
<ol start="2">
<li><strong>It is comparable by type and value</strong></li>
</ol>
<pre><code class="lang-ruby">Price = Data.define(<span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">:currency</span>)

price1 = Price.new(<span class="hljs-symbol">amount:</span> <span class="hljs-number">50</span>, <span class="hljs-symbol">currency:</span> <span class="hljs-string">"USD"</span>)

price2 = Price.new(<span class="hljs-symbol">amount:</span> <span class="hljs-number">40</span>, <span class="hljs-symbol">currency:</span> <span class="hljs-string">"USD"</span>)

price3 = Price.new(<span class="hljs-symbol">amount:</span> <span class="hljs-number">50</span>, <span class="hljs-symbol">currency:</span> <span class="hljs-string">"USD"</span>)

<span class="hljs-comment"># While these are all different objects</span>
puts price1.object_id
puts price2.object_id
puts price3.object_id

<span class="hljs-comment"># You will notice that price1 and price3 will be equal</span>
price1 == price3 <span class="hljs-comment"># true</span>
price1 == price2 <span class="hljs-comment"># false</span>
</code></pre>
<p>It has some other properties but I think these two are the main ones that we should think about when using Data as a value object.</p>
<h3 id="heading-changing-one-or-many-values">Changing one or many values</h3>
<p>The only way to achieve this is to use <code>with</code> and create a new value object:</p>
<pre><code class="lang-ruby">price_a = Price.new(<span class="hljs-symbol">amount:</span> <span class="hljs-number">50</span>, <span class="hljs-symbol">currency:</span> <span class="hljs-string">"USD"</span>)

price_b = price_a.with(<span class="hljs-symbol">amount:</span> <span class="hljs-number">100</span>)
<span class="hljs-comment"># =&gt; #&lt;data Price amount=100, currency="USD"&gt;</span>
</code></pre>
<h3 id="heading-defining-custom-methods">Defining custom methods</h3>
<p>You might notice that if you browse <a target="_blank" href="https://docs.ruby-lang.org/en/master/Data.html">the documentation</a> that the Data object has a limited number of methods: there are 10 methods specific to this object (not taking into consideration of course methods inherited from its ancestors: <code>[Object, Kernel, BasicObject]</code>)</p>
<p>But you can define custom computed properties on it if you want to by passing a block to <code>define</code>:</p>
<pre><code class="lang-ruby">Price = Data.define(<span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">:currency</span>) <span class="hljs-keyword">do</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">to_s</span> </span>
        <span class="hljs-string">"<span class="hljs-subst">#{currency}</span> <span class="hljs-subst">#{amount}</span>"</span>
    <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

price = Price.new(<span class="hljs-symbol">amount:</span> <span class="hljs-number">50</span>, <span class="hljs-symbol">currency:</span> <span class="hljs-string">"USD"</span>)
price.to_s <span class="hljs-comment"># =&gt; "USD 50"</span>
</code></pre>
<h3 id="heading-default-values-using-an-initializer">Default values using an initializer</h3>
<p>In case you want to have default values, you can override the initializer like this:</p>
<pre><code class="lang-ruby">Price = Data.define(<span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">:currency</span>) <span class="hljs-keyword">do</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(<span class="hljs-symbol">amount:</span>, <span class="hljs-symbol">currency:</span> <span class="hljs-string">"USD"</span>)</span></span> 
        <span class="hljs-keyword">super</span>
    <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

<span class="hljs-comment"># with the shortest form possible, like</span>
Price = Data.define(<span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">:currency</span>) <span class="hljs-keyword">do</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(<span class="hljs-symbol">amount:</span>, <span class="hljs-symbol">currency:</span> <span class="hljs-string">"USD"</span>)</span></span> = <span class="hljs-keyword">super</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-checking-the-kind-or-type-of-the-attributes">Checking the kind (or type) of the attributes</h3>
<p>While this is not a feature of Data object, I feel like it is working nice along with it: you can check for type (or, better said, kind of the arguments) using Pattern Matching:</p>
<pre><code class="lang-ruby">Price = Data.define(<span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">:currency</span>) <span class="hljs-keyword">do</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(<span class="hljs-symbol">amount:</span>, <span class="hljs-symbol">currency:</span>)</span></span>
        amount =&gt; Integer
        currency =&gt; String

        <span class="hljs-keyword">super</span>
    <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>This can be useful if you, for example want to define Currency as:</p>
<pre><code class="lang-ruby">Currency = Data.define(<span class="hljs-symbol">:code</span>, <span class="hljs-symbol">:name</span>, <span class="hljs-symbol">:symbol</span>, <span class="hljs-symbol">:decimal</span>)
USD = Currency.new(<span class="hljs-symbol">code:</span> <span class="hljs-string">"USD"</span>, <span class="hljs-symbol">name:</span> <span class="hljs-string">"US Dollar"</span>, <span class="hljs-symbol">symbol:</span> <span class="hljs-string">"$"</span>, <span class="hljs-symbol">decimal:</span> <span class="hljs-number">2</span>)
EUR = Currency.new(<span class="hljs-symbol">code:</span> <span class="hljs-string">"EUR"</span>, <span class="hljs-symbol">name:</span> <span class="hljs-string">"Euro"</span>, <span class="hljs-symbol">symbol:</span> <span class="hljs-string">"€"</span>, <span class="hljs-symbol">decimal:</span> <span class="hljs-number">2</span>)

Price = Data.define(<span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">:currency</span>) <span class="hljs-keyword">do</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(<span class="hljs-symbol">amount:</span>, <span class="hljs-symbol">currency:</span>)</span></span>
        amount =&gt; Integer
        currency =&gt; Currency

        <span class="hljs-keyword">super</span>
    <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

<span class="hljs-comment"># ✅</span>
p1 = Price.new(<span class="hljs-symbol">amount:</span> <span class="hljs-number">10</span>, <span class="hljs-symbol">currency:</span> USD) 
<span class="hljs-comment"># =&gt; #&lt;data Price amount=10, currency=#&lt;data Currency code="USD", name="US Dollar", symbol="$", decimal=2&gt;&gt;</span>

<span class="hljs-comment"># ❌</span>
p2 = Price.new(<span class="hljs-symbol">amount:</span> <span class="hljs-number">10</span>, <span class="hljs-symbol">currency:</span> <span class="hljs-string">"USD"</span>)
<span class="hljs-comment"># =&gt; in `initialize': "USD": Currency === "USD" does not return true (NoMatchingPatternError)</span>
</code></pre>
<h2 id="heading-data-class-vs-struct">Data class vs Struct</h2>
<p>You can use Struct just as well as using the Data class but it will not create immutable objects. You can of course freeze the instances but the Data object gives that for free.</p>
<p>In case you want to create a Struct here is how it might look like:</p>
<pre><code class="lang-ruby">Price = Struct.new(<span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">:currency</span>, <span class="hljs-symbol">keyword_init:</span> <span class="hljs-literal">true</span>)

price = Price.new(<span class="hljs-symbol">amount:</span> <span class="hljs-number">50</span>, <span class="hljs-symbol">currency:</span> <span class="hljs-string">"USD"</span>)
<span class="hljs-comment"># =&gt; &lt;struct Price amount=50, currency="USD"&gt;</span>
</code></pre>
<p>It supports also providing a block when creating the structure to add extra methods as we did it for the Data.</p>
<h2 id="heading-a-bit-on-the-immutability">A bit on the immutability</h2>
<p>Please note that the Data objects are frozen but not deep frozen. That means that if you put inside a Data object attribute a structure that is not immutable then you can change those properties.</p>
<p><strong>My advice is to combine Data objects only with Data objects or immutable or immediate objects to make sure that everything remains immutable.</strong></p>
<p>I recommend you to do this:</p>
<pre><code class="lang-ruby">Currency = Data.define(<span class="hljs-symbol">:code</span>, <span class="hljs-symbol">:name</span>, <span class="hljs-symbol">:symbol</span>, <span class="hljs-symbol">:decimal</span>)
Price = Data.define(<span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">:currency</span>)

EUR = Currency.new(<span class="hljs-symbol">code:</span> <span class="hljs-string">"EUR"</span>, <span class="hljs-symbol">name:</span> <span class="hljs-string">"EURO"</span>, <span class="hljs-symbol">symbol:</span> <span class="hljs-string">"€"</span>, <span class="hljs-symbol">decimal:</span> <span class="hljs-number">2</span>)
price = Price.new(<span class="hljs-symbol">amount:</span> <span class="hljs-number">50</span>, <span class="hljs-symbol">currency:</span> EUR)

price.currency.code = <span class="hljs-string">"MyEUR"</span>
<span class="hljs-comment"># undefined method 'code=' for an instance of Currency (NoMethodError)</span>
</code></pre>
<p>While if you try to put there an object that can be changed like say a Struct it will allow you to change it:</p>
<pre><code class="lang-ruby">Currency = Struct.new(<span class="hljs-symbol">:code</span>, <span class="hljs-symbol">:name</span>, <span class="hljs-symbol">:symbol</span>, <span class="hljs-symbol">:decimal</span>, <span class="hljs-symbol">keyword_init:</span> <span class="hljs-literal">true</span>)
Price = Data.define(<span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">:currency</span>)

EUR = Currency.new(<span class="hljs-symbol">code:</span> <span class="hljs-string">"EUR"</span>, <span class="hljs-symbol">name:</span> <span class="hljs-string">"EURO"</span>, <span class="hljs-symbol">symbol:</span> <span class="hljs-string">"€"</span>, <span class="hljs-symbol">decimal:</span> <span class="hljs-number">2</span>)
price = Price.new(<span class="hljs-symbol">amount:</span> <span class="hljs-number">50</span>, <span class="hljs-symbol">currency:</span> EUR)
<span class="hljs-comment"># =&gt; #&lt;data Price amount=50, currency=#&lt;struct Currency code="EUR", name="EURO", symbol="€", decimal=2&gt;&gt;</span>

price.currency.code = <span class="hljs-string">"MyEUR"</span>
price
<span class="hljs-comment"># =&gt; #&lt;data Price amount=50, currency=#&lt;struct Currency code="MyEUR", name="EURO", symbol="€", decimal=2&gt;&gt;</span>
</code></pre>
<p>The same goes for putting inside a hash:</p>
<pre><code class="lang-ruby">Price = Data.define(<span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">:currency</span>)
EUR = { <span class="hljs-symbol">code:</span> <span class="hljs-string">"EUR"</span>, <span class="hljs-symbol">name:</span> <span class="hljs-string">"EURO"</span>, <span class="hljs-symbol">symbol:</span> <span class="hljs-string">"€"</span>, <span class="hljs-symbol">decimal:</span> <span class="hljs-number">2</span> }

price = Price.new(<span class="hljs-symbol">amount:</span> <span class="hljs-number">50</span>, <span class="hljs-symbol">currency:</span> EUR)
<span class="hljs-comment"># =&gt; #&lt;data Price amount=50, currency={code: "EUR", name: "EURO", symbol: "€", decimal: 2}&gt;</span>
price.currency[<span class="hljs-symbol">:code</span>] = <span class="hljs-string">"MyEUR"</span>
price
<span class="hljs-comment"># =&gt; #&lt;data Price amount=50, currency={code: "MyEUR", name: "EURO", symbol: "€", decimal: 2}&gt;</span>
</code></pre>
<p>So you need to take care of freezing the objects that you put inside yourself if you want them to remain unchanged.</p>
<h3 id="heading-trick-you-can-use-ractor-shareable-to-deep-freeze-a-data-object"><strong>Trick: You can use Ractor shareable to deep freeze a Data object</strong></h3>
<p>This is probably not the purpose of the Ractor.make_shareable method, but it is a nice trick to try:</p>
<pre><code class="lang-ruby">Currency = Struct.new(<span class="hljs-symbol">:code</span>, <span class="hljs-symbol">:name</span>, <span class="hljs-symbol">:symbol</span>, <span class="hljs-symbol">:decimal</span>, <span class="hljs-symbol">keyword_init:</span> <span class="hljs-literal">true</span>)
Price = Data.define(<span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">:currency</span>)

EUR = Currency.new(<span class="hljs-symbol">code:</span> <span class="hljs-string">"EUR"</span>, <span class="hljs-symbol">name:</span> <span class="hljs-string">"EURO"</span>, <span class="hljs-symbol">symbol:</span> <span class="hljs-string">"€"</span>, <span class="hljs-symbol">decimal:</span> <span class="hljs-number">2</span>)
price = Ractor.make_shareable(Price.new(<span class="hljs-symbol">amount:</span> <span class="hljs-number">50</span>, <span class="hljs-symbol">currency:</span> EUR))
<span class="hljs-comment"># =&gt; #&lt;data Price amount=50, </span>
<span class="hljs-comment"># =&gt; currency=#&lt;struct Currency code="EUR", name="EURO", symbol="€", decimal=2&gt;&gt;</span>

price.currency.code = <span class="hljs-string">"MyEUR"</span>
<span class="hljs-comment"># =&gt; in '&lt;main&gt;': can't modify frozen </span>
<span class="hljs-comment"># =&gt; Currency: #&lt;struct Currency code="EUR", name="EURO", symbol="€", decimal=2&gt; (FrozenError)</span>
</code></pre>
<h2 id="heading-some-conclusions">Some conclusions</h2>
<p>I think the Data is a great addition to the Ruby language and I try to reach out to it everytime I need to create a value object. I like the immutability and the comparison by type and value that is created by default.</p>
<p>It also defines <code>deconstruct</code> and <code>deconstruct_keys</code> if you want to use the Data object in pattern matching (Struct defines them too in case you want to use Struct).</p>
<h2 id="heading-read-more">Read more</h2>
<p>I published a followup article where I show some <a target="_blank" href="https://allaboutcoding.ghinda.com/example-of-value-objects-using-rubys-data-class">real world examples of value objects created with the Data class</a>.</p>
<p>If you want to read more about this I recommend at least two resources:</p>
<ul>
<li><p>The feature request on the Ruby Bug Tracker: <a target="_blank" href="https://bugs.ruby-lang.org/issues/16122">https://bugs.ruby-lang.org/issues/16122</a></p>
</li>
<li><p>The great documentation about it maintain by Victor Shepelev here <a target="_blank" href="https://rubyreferences.github.io/rubychanges/3.2.html#data-new-immutable-value-object-class">https://rubyreferences.github.io/rubychanges/3.2.html#data-new-immutable-value-object-class</a></p>
</li>
<li><p>This article <a target="_blank" href="https://thoughtbot.com/blog/value-object-semantics-in-ruby">https://thoughtbot.com/blog/value-object-semantics-in-ruby</a> that goes into details about comparabilityt of two value objects</p>
</li>
</ul>
<hr />
<p>👉 If you like this article and want it in your inbox each week, <a target="_blank" href="https://newsletter.lucianghinda.com">subscribe to my newsletter</a>. You’ll find <strong>ideas on Ruby, software development, software testing, building products and workshops</strong>, plus notes on creativity, tech trends, and whatever else sparks my curiosity.</p>
<p>👐 Want to improve your <strong>developer testing skills</strong>? Visit <a target="_blank" href="https://goodenoughtesting.com/articles">goodenoughtesting.com/articles</a> to discover resources on testing for developers.</p>
<p>👉 <a target="_blank" href="https://newsletter.shortruby.com">Join my Short Ruby Newsletter</a> for weekly Ruby updates and visit rubyandrails.info, a directory of Ruby learning content.</p>
<p>🤝 Connect with me on <a target="_blank" href="https://linkedin.com/in/lucianghinda">Linkedin</a>, <a target="_blank" href="https://bsky.app/profile/lucianghinda.com">Bluesky</a>, <a target="_blank" href="https://ruby.social/@lucian">Ruby.social</a>, , and <a target="_blank" href="https://x.com/lucianghinda">Twitter</a>, where I mostly post about Ruby and Ruby on Rails.</p>
<p>🎥 Follow <a target="_blank" href="https://www.youtube.com/@shortruby">my YouTube channel</a> for short videos about Ruby and Rails.</p>
]]></content:encoded></item><item><title><![CDATA[How to implement JSON-LD Schema for your blog]]></title><description><![CDATA[I will concentrate on how to implement JSON-LD schema for your blog; however, you can apply the examples here to enhance your product or service website.
What is JSON-LD?
JSON-LD stands for JavaScript Object Notation for Linked Data. One use is addin...]]></description><link>https://allaboutcoding.ghinda.com/how-to-implement-json-ld-schema-for-your-blog</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/how-to-implement-json-ld-schema-for-your-blog</guid><category><![CDATA[Blogging]]></category><category><![CDATA[SEO for Developers]]></category><category><![CDATA[marketing]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Fri, 14 Mar 2025 09:28:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741944468141/fe0c7401-235b-4cf3-b95d-8630f5fad31a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I will concentrate on how to implement JSON-LD schema for your blog; however, you can apply the examples here to enhance your product or service website.</p>
<h2 id="heading-what-is-json-ld"><strong>What is JSON-LD?</strong></h2>
<p>JSON-LD stands for JavaScript Object Notation for Linked Data. One use is adding it to web pages, allowing search engines or crawlers to understand the content structure and context better.</p>
<p>It is usually added in the <code>&lt;head&gt;</code> html element like this:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"application/ld+json"</span>&gt;</span><span class="javascript">
{
  <span class="hljs-string">"@context"</span>: <span class="hljs-string">"https://schema.org"</span>,
  <span class="hljs-string">"@type"</span>: <span class="hljs-string">"WebSite"</span>,
  <span class="hljs-string">"url"</span>: <span class="hljs-string">"http://example.com/"</span>
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<h2 id="heading-why-use-json-ld-schema-in-your-blog"><strong>Why use JSON-LD Schema in your blog</strong></h2>
<ul>
<li><p>Your content may show up as rich snippets in search results.</p>
</li>
<li><p>Helps search engines to understand the data related to your blog post as you intend and with the required context.</p>
</li>
<li><p>It may help AI to get structured data about the context of your blog post.</p>
</li>
<li><p>Around 30% (source: <a target="_blank" href="https://searchenginejournal.com/">searchenginejournal.com</a>) of websites are using JSON-JD schema and adding it will improve your SEO results.</p>
</li>
<li><p>When adding <code>sameAs</code> attribute for your <code>author</code> it might improve your presence online</p>
</li>
</ul>
<p>But there are better blog posts that describes why is good to add JSON-LD schema:</p>
<ul>
<li><p><a target="_blank" href="https://www.semrush.com/blog/schema-markup">https://www.semrush.com/blog/schema-markup</a></p>
</li>
<li><p><a target="_blank" href="https://www.schemaapp.com/schema-markup/benefits-of-schema-markup/">https://www.schemaapp.com/schema-markup/benefits-of-schema-markup/</a></p>
</li>
</ul>
<p>Here is a screenshot from <a target="_blank" href="https://www.semrush.com/blog/schema-markup">https://www.semrush.com/blog/schema-markup</a>:</p>
<p><a target="_blank" href="https://www.semrush.com/blog/schema-markup"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741942933519/f105849b-73f7-4484-b15a-6180e1a32127.png" alt="Screenshot about why JSON-LD schema is important" class="image--center mx-auto" /></a></p>
<p>Google also <a target="_blank" href="https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data#supported-formats">recommends using JSON-LD schema</a>:</p>
<p><a target="_blank" href="https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data#supported-formats"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741943002716/1ca5402e-7717-42a4-810f-deb6aa6a2a81.png" alt="https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data#supported-formats" class="image--center mx-auto" /></a></p>
<h2 id="heading-adding-a-json-ld-schema-for-every-page"><strong>Adding a JSON-LD schema for every page</strong></h2>
<p>We should start by adding a schema for every page on your blog. <a target="_blank" href="https://schema.org/WebSite">https://schema.org/WebSite</a> is a schema specification that you should add on every page of your blog (remember this should be inside <code>&lt;script type="application/ld+json"&gt;</code> but to make the code highlight nicer I will only show the JSON part)</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"@context"</span>: <span class="hljs-string">"https://schema.org"</span>,
  <span class="hljs-attr">"@type"</span>: <span class="hljs-string">"WebSite"</span>,
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Lucian Ghinda - Notes"</span>,
  <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://allaboutcoding.ghinda.com"</span>,
  <span class="hljs-attr">"author"</span>: {
    <span class="hljs-attr">"@type"</span>: <span class="hljs-string">"Person"</span>,
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Lucian Ghinda"</span>,
  }
}
</code></pre>
<h3 id="heading-add-url-to-the-author"><strong>Add `url` to the author</strong></h3>
<p>Including an <code>url</code> in the author section helps identify the author's page on your website and aids search engines in recognizing the author's presence on your site, which could enhance the visibility of their content:</p>
<pre><code class="lang-diff">{
  // the other JSON-LD keys
  "author": {
    "@type": "Person",
    "name": "Lucian Ghinda",
<span class="hljs-addition">+    "url": "https://allaboutcoding.ghinda.com/authors/lucian-ghinda"</span>
  }
}
</code></pre>
<h3 id="heading-add-sameas-if-you-want-to-connect-your-website-with-other-places"><strong>Add `sameAs` if you want to connect your website with other places</strong></h3>
<p>Utilize sameAs to show that two entities are fundamentally identical. Within the context of an author section, it links the author's profile on your website to their profiles on other platforms, such as social media or other locations where you maintain a page:</p>
<pre><code class="lang-diff">{
  // the other JSON-LD keys
  "author": {
    "@type": "Person",
    "name": "Lucian Ghinda",
    "url": "https://allaboutcoding.ghinda.com/authors/lucian-ghinda",
<span class="hljs-addition">+    "sameAs": [</span>
<span class="hljs-addition">+      "https://bsky.app/profile/lucianghinda.com",</span>
<span class="hljs-addition">+      "https://www.linkedin.com/in/lucianghinda"</span>
<span class="hljs-addition">+    ]</span>
  }
}
</code></pre>
<h3 id="heading-a-recommended-example-of-the-schema-for-every-page-on-your-blog">A recommended example of the schema for every page on your blog:</h3>
<pre><code class="lang-json">{
  <span class="hljs-attr">"@context"</span>: <span class="hljs-string">"https://schema.org"</span>,
  <span class="hljs-attr">"@type"</span>: <span class="hljs-string">"WebSite"</span>,
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Lucian Ghinda - Notes"</span>,
  <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://allaboutcoding.ghinda.com"</span>,
  <span class="hljs-attr">"author"</span>: {
    <span class="hljs-attr">"@type"</span>: <span class="hljs-string">"Person"</span>,
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Lucian Ghinda"</span>,
    <span class="hljs-attr">"sameAs"</span>: [
       <span class="hljs-string">"https://bsky.app/profile/lucianghinda.com"</span>,
        <span class="hljs-string">"https://www.linkedin.com/in/lucianghinda"</span>
    ]
  }
}
</code></pre>
<h2 id="heading-json-ld-schema-for-inside-the-blog-post-article"><strong>JSON-LD Schema for inside the blog post / article</strong></h2>
<p>You can add multiple JSON-LD script tags in your website. You should keep the main one that is about WebSite and add inside the blog posts one about <strong>`BlogPosting` -</strong> <a target="_blank" href="https://schema.org/BlogPosting"><strong>https://schema.org/BlogPosting</strong></a></p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"application/ld+json"</span>&gt;</span><span class="javascript">
{ 
  <span class="hljs-string">"@context"</span>: <span class="hljs-string">"https://schema.org"</span>,
  <span class="hljs-string">"@type"</span>: <span class="hljs-string">"BlogPosting"</span>,
  <span class="hljs-string">"mainEntityOfPage"</span>: {
    <span class="hljs-string">"@type"</span>: <span class="hljs-string">"WebPage"</span>,
    <span class="hljs-string">"@id"</span>: <span class="hljs-string">"https://allaboutcoding.ghinda.com/post-url"</span>
  },
  <span class="hljs-string">"headline"</span>: <span class="hljs-string">"Why add JSON+LD to your blogpost"</span>,
  <span class="hljs-string">"datePublished"</span>: <span class="hljs-string">"2025-03-11T13:22:00+02:00"</span>,
  <span class="hljs-string">"dateModified"</span>: <span class="hljs-string">"2025-03-11T14:30:00+02:00"</span>,
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<h3 id="heading-a-bit-about-datepublished-and-datemodified"><strong>A bit about datePublished and dateModified</strong></h3>
<p>Values for these two fields can be Date or DateTime. Choose one that fits you bets or that you can generate easily.</p>
<p>In case you usually don't update your articles, then only add `datePublished`</p>
<pre><code class="lang-diff">
{ 
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://allaboutcoding.ghinda.com/post-url"
  },
  "headline": "Why add JSON+LD to your blogpost",
<span class="hljs-addition">+  "datePublished": "2025-03-11T13:22:00+02:00",</span>
<span class="hljs-addition">+  "dateModified": "2025-03-11T14:30:00+02:00"</span>
}
</code></pre>
<h3 id="heading-then-add-author"><strong>Then add author</strong></h3>
<p>You can use the same snippet as you used in the WebSite schema:</p>
<pre><code class="lang-json">{
  <span class="hljs-comment">// the other JSON-LD keys</span>
  <span class="hljs-attr">"author"</span>: {
    <span class="hljs-attr">"@type"</span>: <span class="hljs-string">"Person"</span>,
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Lucian Ghinda"</span>,
    <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://allaboutcoding.ghinda.com/authors/lucian-ghinda"</span>,
    <span class="hljs-attr">"sameAs"</span>: [
      <span class="hljs-string">"https://bsky.app/profile/lucianghinda.com"</span>,
      <span class="hljs-string">"https://www.linkedin.com/in/lucianghinda"</span>
    ]
  }
}
</code></pre>
<p>If you have multiple authors, then make the "author" key an array:</p>
<pre><code class="lang-json">{
  <span class="hljs-comment">// the other JSON-LD keys</span>
  <span class="hljs-attr">"author"</span>: [
    { 
      <span class="hljs-attr">"@type"</span>: <span class="hljs-string">"Person"</span>, 
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Lucian Ghinda"</span>,
      <span class="hljs-comment">// other attributes</span>
    },  
    { 
      <span class="hljs-attr">"@type"</span>: <span class="hljs-string">"Organization"</span>, 
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Short Ruby"</span>,
      <span class="hljs-comment">// other attributes</span>
    }  
  ]
}
</code></pre>
<h3 id="heading-json-ld-schema-for-a-blog-post-add-publisher"><strong>JSON-LD Schema for a blog post - add publisher</strong></h3>
<p>The <code>publisher</code> key identifies the publisher of the creative work. If you have your own blog, you can list yourself as the publisher. However, if you're writing for an organization, make sure to include the organization's name instead:</p>
<pre><code class="lang-json">{
  <span class="hljs-comment">// the other JSON-LD keys</span>
  <span class="hljs-attr">"publisher"</span>: {
    <span class="hljs-attr">"@type"</span>: <span class="hljs-string">"Organization"</span>,
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Short Ruby"</span>,
    <span class="hljs-attr">"logo"</span>: {
      <span class="hljs-attr">"@type"</span>: <span class="hljs-string">"ImageObject"</span>,
      <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://shortruby.com/assets/logo-d8627cb3e82564f3328709589c3c1ce34b81d0afed316a0b58d27ab93bcaa9d4.png"</span>
    }
  },
}
</code></pre>
<h3 id="heading-the-final-json-ld-for-the-blog-post">The final JSON-LD for the blog post</h3>
<p>Here is how the JSON-LD schema for a blog post might look like:</p>
<pre><code class="lang-json">{ 
  <span class="hljs-attr">"@context"</span>: <span class="hljs-string">"https://schema.org"</span>,
  <span class="hljs-attr">"@type"</span>: <span class="hljs-string">"BlogPosting"</span>,
  <span class="hljs-attr">"mainEntityOfPage"</span>: {
    <span class="hljs-attr">"@type"</span>: <span class="hljs-string">"WebPage"</span>,
    <span class="hljs-attr">"@id"</span>: <span class="hljs-string">"https://allaboutcoding.ghinda.com/post-url"</span>
  },
  <span class="hljs-attr">"headline"</span>: <span class="hljs-string">"Why add JSON+LD to your blogpost"</span>,
  <span class="hljs-attr">"datePublished"</span>: <span class="hljs-string">"2025-03-11T13:22:00+02:00"</span>,
  <span class="hljs-attr">"dateModified"</span>: <span class="hljs-string">"2025-03-11T14:30:00+02:00"</span>,
  <span class="hljs-attr">"author"</span>: {
    <span class="hljs-attr">"@type"</span>: <span class="hljs-string">"Person"</span>,
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Lucian Ghinda"</span>,
    <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://allaboutcoding.ghinda.com/authors/lucian-ghinda"</span>,
    <span class="hljs-attr">"sameAs"</span>: [
      <span class="hljs-string">"https://bsky.app/profile/lucianghinda.com"</span>,
      <span class="hljs-string">"https://www.linkedin.com/in/lucianghinda"</span>
    ]
  },
  <span class="hljs-attr">"publisher"</span>: {
    <span class="hljs-attr">"@type"</span>: <span class="hljs-string">"Organization"</span>,
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Short Ruby"</span>,
    <span class="hljs-attr">"logo"</span>: {
      <span class="hljs-attr">"@type"</span>: <span class="hljs-string">"ImageObject"</span>,
      <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://www.exampleblog.com/logo.png"</span>
    }
  }
}
</code></pre>
<h2 id="heading-tools">Tools</h2>
<p>There are many more variations of how to combine various items and this way communicate to a crawler or search engine what you want them to know about your blog.</p>
<p>Here are some tools you can use to validate your JSON-LD schema and check how Google parses rich snippets:</p>
<p><a target="_blank" href="https://search.google.com/test/rich-results">https://search.google.com/test/rich-results</a></p>
<p><a target="_blank" href="https://validator.schema.org">https://validator.schema.org</a></p>
<h2 id="heading-further-reading">Further reading</h2>
<p>Avo published a very nice article about how to add the schema markup on a Rails app → <a target="_blank" href="https://avohq.io/blog/structured-data-rails"><strong>Adding Structured Data to a Rails application</strong></a></p>
<hr />
<p>If you like this article:</p>
<p>👐 Interested in learning how to improve your developer testing skills? Join my live online workshop about <a target="_blank" href="http://goodenoughtesting.com/"><strong>goodenoughtesting.com</strong></a> <strong>- to learn test design techniques for writing effective tests</strong></p>
<p>👉 Join my <a target="_blank" href="https://newsletter.shortruby.com/"><strong>Short Ruby Newsletter</strong></a> for weekly Ruby updates from the community and visit <a target="_blank" href="http://rubyandrails.info/"><strong>rubyandrails.info</strong></a><strong>, a directory with learning content about Ruby.</strong></p>
<p>🤝 Let's connect on <a target="_blank" href="https://bsky.app/profile/lucianghinda.com"><strong>Bluesky</strong></a>, <a target="_blank" href="http://ruby.social/"><strong>Ruby.social</strong></a><strong>,</strong> <a target="_blank" href="https://linkedin.com/in/lucianghinda"><strong>Linkedin</strong></a><strong>,</strong> <a target="_blank" href="https://x.com/lucianghinda"><strong>Twitter</strong></a> <strong>where I post mostly about Ruby and Ruby on Rails.</strong></p>
<p>🎥 Follow me on <a target="_blank" href="https://www.youtube.com/@shortruby"><strong>my YouTube channel</strong></a> for short videos about Ruby/Rails</p>
]]></content:encoded></item><item><title><![CDATA[How to make sure you review your monkey patch when updating Ruby gems]]></title><description><![CDATA[When I installed Writebook for booklet.goodenoughtesting.com, I monkey-patched some parts of it because I wanted to remove the creation of the first book. Later, I added some default meta tags. You can read about my approach in the article “Overridin...]]></description><link>https://allaboutcoding.ghinda.com/how-to-make-sure-you-review-your-monkey-patch-when-updating-ruby-gems</link><guid isPermaLink="true">https://allaboutcoding.ghinda.com/how-to-make-sure-you-review-your-monkey-patch-when-updating-ruby-gems</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Updates]]></category><dc:creator><![CDATA[Lucian Ghinda]]></dc:creator><pubDate>Fri, 21 Feb 2025 08:23:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1740126169588/fe66b63a-423e-4a16-83b7-7013b222d0d0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I installed Writebook for <a target="_blank" href="https://booklet.goodenoughtesting.com">booklet.goodenoughtesting.com</a>, I monkey-patched some parts of it because I wanted to remove the creation of the first book. Later, I added some default meta tags. You can read about my approach in the article <a target="_blank" href="https://allaboutcoding.ghinda.com/overriding-methods-in-ruby-on-rails-a-no-code-editing-approach">“Overriding Methods in Ruby on Rails: A No-Code-Editing</a> Approach.” You might monkey patch a gem or a part of Rails for different reasons.</p>
<p>I agree that monkey patching should be the last approach and used carefully. One of the most critical issues is that the code you create depends on the <em>structure</em> of the code you are monkey patching, thus voluntarily violating the contract that the gem's author offered.</p>
<p>I see monkey patching as a breach of contract with the author of a gem. In rare cases when this is necessary, the fix should be on the side that breached the contract. If you go down this route, then every time the gem is updated, you have to review the code of the gem and test your monkey patch to see if everything works as you expect it.</p>
<h2 id="heading-how-to-automate-the-check-of-patched-gem-version">How to automate the check of patched Gem version</h2>
<p>Add the following code at the beginning of your monkey patch file.</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># This is an example about ActiveSupport</span>

<span class="hljs-keyword">if</span> Rails.env.local? <span class="hljs-params">||</span> Rails.env.ci?
  <span class="hljs-keyword">if</span> ActiveSupport.version &gt; Gem::Version.new(<span class="hljs-string">'8.0.0'</span>)
    raise <span class="hljs-string">"\n\nReview this monkey patch after upgrading \n"</span> \
          <span class="hljs-string">"Path: <span class="hljs-subst">#{__FILE_<span class="hljs-number">_</span>}</span>"</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>You can do the same if you have an initializer and you <em>temporary</em> added some settings that you know you want to remove or review after doing an update.</p>
<p>I found this approach in various projects I’ve worked on over the past couple of years. It can take different forms: sometimes only in CI, sometimes in a specific CI that checks compatibility, sometimes in tests, and other variations.</p>
<p>In case the gem does not define the version as an <code>Gem::Version</code> object and it might define it like this:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">MyGem</span></span>
  VERSION = <span class="hljs-string">'1.0.1'</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Then you should create the comparison in the following way:</p>
<pre><code class="lang-diff">if Rails.env.local? || Rails.env.ci?
<span class="hljs-deletion">- if MyGem::Version &gt; Gem::Version.new('8.0.0')</span>
<span class="hljs-addition">+  if Gem::Version.new(MyGem::VERSION) &gt; Gem::Version.new('1.0.1')</span>
    raise "\n\nReview this monkey patch after upgrading \n" \
          "Path: #{__FILE__}"
  end
end
</code></pre>
<p>You should always compare with <code>Gem::Version</code> object because it defines the method <code>&lt;=&gt;</code> in a way that takes into consideration major, minor and patch version. Here is how that method currently looks like:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># Source: https://github.com/rubygems/rubygems/blob/master/lib/rubygems/version.rb#L360</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">&lt;=&gt;</span><span class="hljs-params">(other)</span></span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">self</span> &lt;=&gt; <span class="hljs-keyword">self</span>.<span class="hljs-keyword">class</span>.new(other) <span class="hljs-keyword">if</span> (String === other) &amp;&amp; <span class="hljs-keyword">self</span>.<span class="hljs-keyword">class</span>.correct?(other)

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">unless</span> Gem::Version === other
    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> <span class="hljs-keyword">if</span> @version == other.version <span class="hljs-params">||</span> canonical_segments == other.canonical_segments

    lhsegments = canonical_segments
    rhsegments = other.canonical_segments

    lhsize = lhsegments.size
    rhsize = rhsegments.size
    limit  = (lhsize &gt; rhsize ? lhsize : rhsize) - <span class="hljs-number">1</span>

    i = <span class="hljs-number">0</span>

    <span class="hljs-keyword">while</span> i &lt;= limit
      lhs = lhsegments[i] <span class="hljs-params">||</span> <span class="hljs-number">0</span>
      rhs = rhsegments[i] <span class="hljs-params">||</span> <span class="hljs-number">0</span>
      i += <span class="hljs-number">1</span>

      <span class="hljs-keyword">next</span>      <span class="hljs-keyword">if</span> lhs == rhs
      <span class="hljs-keyword">return</span> -<span class="hljs-number">1</span> <span class="hljs-keyword">if</span> String  === lhs &amp;&amp; Numeric === rhs
      <span class="hljs-keyword">return</span>  <span class="hljs-number">1</span> <span class="hljs-keyword">if</span> Numeric === lhs &amp;&amp; String  === rhs

      <span class="hljs-keyword">return</span> lhs &lt;=&gt; rhs
    <span class="hljs-keyword">end</span>

    <span class="hljs-number">0</span>
  <span class="hljs-keyword">end</span>
</code></pre>
<p>As you can notice it knows also how to compare with a <code>String</code> so you could probably write this code like this:</p>
<pre><code class="lang-ruby"><span class="hljs-keyword">if</span> Rails.env.local? <span class="hljs-params">||</span> Rails.env.ci?
  <span class="hljs-keyword">if</span> Gem::Version.new(<span class="hljs-string">"1.0.1"</span>) &lt;= MyGem::VERSION
    raise <span class="hljs-string">"\n\nReview this monkey patch after upgrading \n"</span> \
          <span class="hljs-string">"Path: <span class="hljs-subst">#{__FILE_<span class="hljs-number">_</span>}</span>"</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>But I prefer to make sure that both of them are <code>Gem::Version</code> objects.</p>
<hr />
<p>If you like this article:</p>
<p>👐 Interested in learning how to improve your developer testing skills? Join my live online workshop about <a target="_blank" href="http://goodenoughtesting.com/"><strong>goodenoughtesting.com</strong></a> <strong>- to learn test design techniques for writing effective tests</strong></p>
<p>👉 Join my <a target="_blank" href="https://newsletter.shortruby.com/"><strong>Short Ruby Newsletter</strong></a> for weekly Ruby updates from the community and visit <a target="_blank" href="http://rubyandrails.info/"><strong>rubyandrails.info</strong></a><strong>, a directory with learning content about Ruby.</strong></p>
<p>🤝 Let's connect on <a target="_blank" href="https://bsky.app/profile/lucianghinda.com"><strong>Bluesky</strong></a>, <a target="_blank" href="http://ruby.social/"><strong>Ruby.social</strong></a><strong>,</strong> <a target="_blank" href="https://linkedin.com/in/lucianghinda"><strong>Linkedin</strong></a><strong>,</strong> <a target="_blank" href="https://x.com/lucianghinda"><strong>Twitter</strong></a> <strong>where I post mostly about Ruby and Ruby on Rails.</strong></p>
<p>🎥 Follow me on <a target="_blank" href="https://www.youtube.com/@shortruby"><strong>my YouTube channel</strong></a> for short videos about Ruby/Rails</p>
]]></content:encoded></item></channel></rss>