Project

shiboru

0.0
No release in over 3 years
Shiboru provides a clean and intuitive way to handle filtering, ordering, and pagination in Ruby applications. It automatically maps models to filter classes, parses HTTP parameters, and supports complex nested associations with various operators.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies
 Project Readme

Shiboru

DRF-style filtering for Rails APIs that feels like home to anyone coming from Django. If you’ve used DjangoFilter, this will click instantly.

Ransack exists and is great for many Rails apps, but for teams steeped in Django/DRF conventions the mental model never quite felt native. Shiboru was built to bring the DjangoFilter ergonomics to Rails: field__op=value params, association paths like user__email__icontains=..., first-class ordering and pagination, and per-model FilterSet classes with a sweet, explicit DSL.

Maintainers: Rishi Banerjee, Pratik Jain, Nikhil Anand, Dhruv Bhargava

Features

  • Per-model FilterSet classes: UserFilter, Profiles::UserFilter, inferred from class name.

  • DRF/DjangoFilter-style operators:

    • __eq, __ne, __gt, __gte, __lt, __lte
    • __contains, __icontains, __startswith, __istartswith, __endswith, __iendswith
    • __in, __nin, __isnull, __range
  • Nested association paths: profile__city__icontains=..., company__name__eq=....

  • Ordering: ?ordering=-created_at,name.

  • Pagination: page/page_size or limit/offset.

  • Whitelist DSL: fields, related_fields, orderable_fields.

  • Custom filters per resource: filter :q { |scope, v| ... }.

  • Postgres-friendly case-insensitive ops via ILIKE. Optional pg_trgm migration for performance.

Installation

Add to your Rails app:

# Gemfile
gem "shiboru", git: "https://github.com/your-org/shiboru" # or path: "../shiboru"

Install and run the installer:

bundle install
bin/spring stop
bin/rails g shiboru:install --pgtrgm   # --pgtrgm is safe; no-ops on non-Postgres adapters
bin/rails db:migrate

The installer will create:

  • config/initializers/shiboru.rb
  • app/filters/.keep
  • Optionally a pg_trgm migration guarded to run only on Postgres

Quick start

Create a model:

bin/rails g model User name:string email:string:index age:integer active:boolean company:string signed_up_at:datetime
bin/rails db:migrate

Generate a filter:

bin/rails g shiboru:filter UserFilter

Edit app/filters/user_filter.rb:

# frozen_string_literal: true
class UserFilter < Shiboru::FilterSet
  # Model is inferred: User

  fields :id, :name, :email, :age, :active, :company, :signed_up_at, :created_at, :updated_at
  orderable_fields :name, :age, :company, :signed_up_at, :created_at

  # Optional quick search (?q=...)
  filter :q do |scope, value, context:|
    v = "%#{value}%"
    scope.where("users.name ILIKE ? OR users.email ILIKE ?", v, v)
  end
end

Controller and routes:

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  include Shiboru::Controller

  def index
    render json: api_index(User, params)   # returns {count,next,previous,results}
  end
end
# config/routes.rb
Rails.application.routes.draw do
  resources :users, only: [:index]
end

Seed a little data (optional):

# db/seeds.rb
%w[Acme Omnix Finlyt NovaLabs].each do |co|
  5.times do |i|
    User.create!(
      name: ["Rishi Banerjee", "Pratik Jain", "Nikhil Anand", "Dhruv Bhargava"].sample + " #{i}",
      email: "user#{i}+#{co.downcase}@example.com",
      age: rand(18..60),
      active: [true, false].sample,
      company: co,
      signed_up_at: rand(180).days.ago + rand(0..86_400)
    )
  end
end
bin/rails db:seed

Try it:

GET /users?name__icontains=rishi&age__gte=20&ordering=-created_at&page=1&page_size=10
GET /users?q=bhargava

The Filter DSL

Shiboru looks for a FilterSet class named after the model: UserFilter for User, Profiles::UserFilter for Profiles::User. Place filters in app/filters/**.

class ArticleFilter < Shiboru::FilterSet
  fields :id, :title, :status, :views, :published_at, :category, :created_at
  related_fields :user__name, :user__email, :user__company, :user__active
  orderable_fields :published_at, :views, :created_at, :title

  filter :q do |scope, value, context:|
    v = "%#{value}%"
    scope.where("articles.title ILIKE ? OR articles.body ILIKE ?", v, v)
  end
end

Notes:

  • fields whitelists filterable base-table columns.
  • related_fields whitelists association fields via assoc__field paths.
  • orderable_fields whitelists fields that can appear in ?ordering=....

Query language

Any query param that matches path__operator=value becomes a filter.

Operators

  • Equality and comparisons: __eq, __ne, __gt, __gte, __lt, __lte
  • String matching: __contains, __startswith, __endswith (case sensitive)
  • Case-insensitive variants (Postgres): __icontains, __istartswith, __iendswith
  • Membership: __in=1,2,3, __nin=4,5
  • Null checks: __isnull=true|false
  • Ranges: __range=from,to or from..to

Examples:

/users?age__gte=25&age__lt=40
/users?email__icontains=example.com
/users?id__in=1,2,3
/users?signed_up_at__range=2025-01-01,2025-06-30

Associations

/articles?user__company__eq=Acme
/articles?user__active__eq=true&ordering=-published_at,title

Shiboru builds LEFT OUTER JOINs for association chains. If you filter a has_many chain on the “one” side and see duplicates, add distinct at the controller level or we can enable a built-in distinct_on_root toggle later.

Ordering

?ordering=-created_at,name

Multiple fields are comma-separated. Use - for descending. Association paths are supported:

/articles?ordering=-user__name,views

Pagination

Two modes, both supported:

  • Page-based: ?page=2&page_size=50
  • Offset-based: ?limit=50&offset=100

Response envelope matches DRF:

{
  "count": 421,
  "next": 3,        // page number (or next offset in offset mode)
  "previous": 1,    // previous page (or previous offset)
  "results": [ ... ]
}

ENV["API_MAX_LIMIT"] caps maximum page size and limit (default 100).

Controllers

Include the helper and call api_index(Model, params):

class ArticlesController < ApplicationController
  include Shiboru::Controller

  def index
    render json: api_index(Article, params)
  end
end

Optionally add a serializer:

render json: api_index(Article, params, serializer: ArticleSerializer)

Generators

Install:

bin/rails g shiboru:install --pgtrgm

Create a filter:

bin/rails g shiboru:filter UserFilter
bin/rails g shiboru:filter Articles::PostFilter   # for Articles::Post model

Templates live in lib/generators/shiboru/... inside the gem.

Performance

  • Add btree indexes on equality/ordering fields (created_at, foreign keys, etc.).

  • For __icontains and other case-insensitive matchers on Postgres:

    • enable pg_trgm (--pgtrgm in installer),

    • create trigram indexes:

      CREATE EXTENSION IF NOT EXISTS pg_trgm;
      CREATE INDEX CONCURRENTLY idx_users_name_trgm  ON users    USING gin (name gin_trgm_ops);
      CREATE INDEX CONCURRENTLY idx_users_email_trgm ON users    USING gin (email gin_trgm_ops);
      CREATE INDEX CONCURRENTLY idx_articles_title_trgm ON articles USING gin (title gin_trgm_ops);

Security

  • The DSL is a whitelist. Only fields you declare are filterable/orderable.
  • If related_fields is omitted, Shiboru allows all columns on joined targets. For strict security, always declare related_fields explicitly.

Example: Users and Articles

Models:

class User < ApplicationRecord
  has_many :articles, dependent: :destroy
end

class Article < ApplicationRecord
  belongs_to :user
end

Filters:

class UserFilter < Shiboru::FilterSet
  fields :id, :name, :email, :age, :active, :company, :signed_up_at, :created_at
  orderable_fields :name, :age, :company, :signed_up_at, :created_at
  filter :q do |scope, v, context:|
    like = "%#{v}%"
    scope.where("users.name ILIKE ? OR users.email ILIKE ?", like, like)
  end
end

class ArticleFilter < Shiboru::FilterSet
  fields :id, :title, :status, :views, :published_at, :category, :created_at
  related_fields :user__name, :user__email, :user__company, :user__active
  orderable_fields :published_at, :views, :title, :created_at
  filter :q do |scope, v, context:|
    like = "%#{v}%"
    scope.where("articles.title ILIKE ? OR articles.body ILIKE ?", like, like)
  end
end

Requests:

/users?age__gte=25&company__eq=Acme&ordering=-signed_up_at
/users?q=Anand&page=1&page_size=20

/articles?status__in=published,archived&user__company__eq=Acme
/articles?views__gte=1000&ordering=-views,title&limit=30&offset=60

How it works

  • Shiboru::Registry maps ModelModelFilter (namespaced aware).

  • FilterSet:

    • Parses params into filter instructions (path, op, value).
    • Validates against whitelists.
    • Builds LEFT OUTER JOINs for association paths.
    • Applies WHERE based on operators with bind parameters.
    • Applies ORDER BY and pagination.
    • Returns a DRF-like envelope.

Why not just use Ransack?

Ransack is powerful, battle-tested, and a fine choice for many Rails apps. Teams coming from Django/DRF often prefer the field__op=value grammar and the mental model of FilterSet classes that mirror the resource. Shiboru embraces that exact style so Rails APIs can feel like DRF without translation overhead.

Reference: DjangoFilter documentation https://django-filter.readthedocs.io/

Development

  • Ruby 3.1+
  • Rails 6.1+ (tested on 7/8)

Run tests:

bundle exec rspec

License

MIT