A Ruby gem that provides column-based sorting capabilities for Rails models with support for associations and custom scopes.
- Column Sorting: Simple column sorting with direction support
- Association Sorting: Sort by columns in associated models using
association__columnsyntax - Custom Scopes: Support for custom sorting logic via
c_*pattern (backed bysorted_by_*scopes) - Error Handling: Development vs production error handling strategies
- SQL Generation: Proper JOIN handling with NULL value management
- Rails Integration: Automatic Rails integration via Railtie
- Comprehensive Testing: Shared examples for consistent testing across models
Add this line to your application's Gemfile:
gem 'sort_by_columns', git: 'https://github.com/myunio/sort_by_columns.git'And then execute:
bundle installIn your model, include the Saltbox::SortByColumns::Model module and specify which columns can be sorted:
class User < ApplicationRecord
include Saltbox::SortByColumns::Model
belongs_to :organization
has_many :posts
sort_by_columns :name, :email, :created_at, :organization__name
endIn your controller, include the Saltbox::SortByColumns::Controller module:
class UsersController < ApplicationController
include Saltbox::SortByColumns::Controller
def index
@users = apply_scopes(User).page(params[:page])
end
endNote: Make sure you call
apply_scopeson your model scope in the controller action, or the sorting parameters won't be processed.
With everything set up, you can now pass the sort parameter in your API requests:
GET /users?sort=name
GET /users?sort=name:asc
GET /users?sort=name:desc
GET /users?sort=name:asc,created_at:desc
GET /users?sort=organization__name:asc
This gem is built on top of the has_scope gem, which provides the foundation for parameter-based scope application in Rails controllers.
has_scope handles the parameter processing and scope application, while sort_by_columns provides the sorting logic:
- Parameter Processing: has_scope automatically reads the
sortparameter from the request - Scope Definition: This gem automatically defines a
sorted_by_columnsscope on your models - Scope Application: has_scope's
apply_scopesmethod applies the sorting scope with the parameter value - Sorting Logic: This gem processes the sort parameter and generates the appropriate SQL
The apply_scopes method in your controllers comes from has_scope, not from this gem:
class UsersController < ApplicationController
include Saltbox::SortByColumns::Controller # This adds has_scope functionality
def index
# apply_scopes comes from has_scope
# It automatically applies the sorted_by_columns scope when sort param is present
@users = apply_scopes(User).page(params[:page])
end
endWhen you include Saltbox::SortByColumns::Controller, it automatically sets up has_scope for the sort parameter:
# This is done automatically when you include the controller module
has_scope :sorted_by_columns, using: :sort, type: :hashThis means:
- has_scope looks for a
sortparameter in the request - If found, it calls the
sorted_by_columnsscope on your model with the parameter value - This gem provides the
sorted_by_columnsscope implementation on your models
Here's how a request like GET /users?sort=name:desc,organization__name:asc gets processed:
- has_scope extracts
sort=name:desc,organization__name:ascfrom params - has_scope calls
User.sorted_by_columns("name:desc,organization__name:asc") - This gem parses the sort string and generates appropriate SQL with JOINs and ORDER clauses
- has_scope applies the resulting scope to build the final query
has_scope provides several benefits that this gem leverages:
- Automatic parameter binding: No need to manually check for sort parameters
- Scope chaining: Works seamlessly with other scopes and query methods
- Consistent API: Follows Rails conventions for parameter-based filtering
- Flexibility: Easy to combine with other has_scope filters like search, pagination, etc.
For more information about has_scope itself, see the official documentation.
For simple model columns, just pass the column name:
sort_by_columns :name, :email, :created_atAPI usage:
GET /users?sort=name:asc
GET /users?sort=email:desc,created_at:asc
For columns on associated models, use the association__column format:
class User < ApplicationRecord
belongs_to :organization
include Saltbox::SortByColumns::Model
sort_by_columns :name, :organization__name, :organization__created_at
endAPI usage:
GET /users?sort=organization__name:asc
GET /users?sort=name:asc,organization__name:desc
- Uses
LEFT OUTER JOINto ensure records with null associations are included - Handles null values gracefully using
NULLS LAST(for ascending sorts) orNULLS FIRST(for descending sorts) - Supports custom
class_nameassociations by properly using the association name as the table alias
For complex sorting logic, use custom scopes with the c_ prefix. The naming convention is critical: a sortable column named c_* must be backed by a scope named sorted_by_*.
class User < ApplicationRecord
include Saltbox::SortByColumns::Model
has_many :addresses
# The sortable column name: c_full_address
sort_by_columns :name, :c_full_address
# Required scope name: sorted_by_full_address (c_ becomes sorted_by_)
scope :sorted_by_full_address, ->(direction) {
joins(:addresses)
.order("addresses.street #{direction}, addresses.city #{direction}")
}
endc_full_address→sorted_by_full_addressc_total_orders→sorted_by_total_ordersc_latest_activity→sorted_by_latest_activity
API usage:
GET /users?sort=c_full_address:desc
Important: Custom sort columns must be used alone and cannot be combined with other columns. For example,
c_full_address,namewill not work.
The gem handles invalid columns differently based on the environment:
- Raises
ArgumentErrorfor invalid columns to help catch issues during development - Provides detailed error messages with suggested fixes
- Silently ignores invalid columns and logs warnings
- Continues processing valid columns when possible
- For custom scopes, ignores the entire sort operation if invalid
The gem includes a Railtie that automatically integrates with Rails applications:
- Automatic Loading: The gem automatically loads when Rails starts
- Shared Examples: RSpec shared examples are automatically loaded when RSpec is available
- Configuration: Provides
Rails.application.config.saltbox_sort_by_columnsfor future configuration options
The Railtie handles all the integration automatically - you don't need to do anything special.
The gem includes shared examples to help you test your sortable models:
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
it_behaves_like "sortable by columns", {
allowed_columns: [:name, :email, :created_at],
disallowed_column: :password,
associated_column: {
name: :organization__name,
expected_sql: "organizations.name"
}
}
endThe shared examples are automatically loaded when RSpec is available, so you don't need to require them manually.
# Run all tests with coverage
bundle exec rspec
# Run specific test phases
bundle exec rspec spec/saltbox/sort_by_columns/model_basic_spec.rb # Core functionality
bundle exec rspec spec/saltbox/sort_by_columns/model_edge_cases_spec.rb # Edge cases & validation
bundle exec rspec spec/integration/ # Integration tests
# Run with specific output format
bundle exec rspec --format documentationFor complete testing information, see:
- TESTING.md - Comprehensive testing guide including patterns, conventions, and advanced test running options
- EXAMPLES.md - Real-world usage examples for e-commerce, user management, project management, and more
- Environment-Aware Testing: Different behaviors in development vs production
- Security Testing: Parameter pollution and SQL injection prevention
- Performance Testing: Memory efficiency and concurrent request handling
- Real Rails Integration: Uses Combustion framework for authentic Rails testing
- Comprehensive Edge Cases: Unicode handling, malformed inputs, logger failures
- Mock and Integration: Both isolated unit tests and full-stack integration tests
- Rails >= 7.0
- has_scope ~> 0.8
For local development of the gem itself:
# Set up local development
bundle config local.sort_by_columns /path/to/sort_by_columns
bundle install
# Run tests
rake spec
# Run linter
rake standard
# Build the gem
rake buildThe gem includes rake tasks for managing version bumps:
# Check current version
rake version:current
# Preview what the next version would be (no changes made)
rake 'version:preview[patch]' # Preview patch bump (1.0.1 → 1.0.2)
rake 'version:preview[minor]' # Preview minor bump (1.0.1 → 1.1.0)
rake 'version:preview[major]' # Preview major bump (1.0.1 → 2.0.0)
# Bump version (updates version.rb, CHANGELOG.md, commits, and tags)
rake 'version:bump[patch]' # Bump patch version
rake 'version:bump[minor]' # Bump minor version
rake 'version:bump[major]' # Bump major versionNote: The square brackets must be quoted in zsh/bash to prevent shell expansion. Use single quotes around the entire task name.
After bumping, the task will:
- Update
lib/saltbox/sort_by_columns/version.rb - Add a new entry template to
CHANGELOG.md - Create a git commit
- Create an annotated git tag (e.g.,
v1.0.2) - Show you the commands to push to GitHub
Example release workflow:
# 1. Preview the version bump
rake 'version:preview[patch]'
# 2. Bump the version
rake 'version:bump[patch]'
# 3. Edit CHANGELOG.md to fill in the changes
# 4. Push to GitHub
git push origin main --follow-tagsFor development in a Rails application using the gem:
# Set up local development when needed
bundle config local.sort_by_columns /path/to/sort_by_columns
bundle install
# Return to remote gem when done
bundle config --delete local.sort_by_columns
bundle install- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Run the tests (
rake spec) - Run the linter (
rake standard) - Commit your changes (
git commit -am 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This gem is available as open source under the terms of the MIT License.
class Post < ApplicationRecord
include Saltbox::SortByColumns::Model
sort_by_columns :title, :published_at, :view_count
endclass Member < ApplicationRecord
include Saltbox::SortByColumns::Model
belongs_to :organization
belongs_to :current_status, class_name: "Status", optional: true
sort_by_columns :name, :email, :organization__name, :current_status__name
endclass User < ApplicationRecord
include Saltbox::SortByColumns::Model
has_many :orders
# Custom sortable column: c_total_orders
sort_by_columns :name, :email, :c_total_orders
# Required scope: sorted_by_total_orders (c_ becomes sorted_by_)
scope :sorted_by_total_orders, ->(direction) {
joins(:orders)
.group('users.id')
.order("COUNT(orders.id) #{direction}")
}
endclass UsersController < ApplicationController
include Saltbox::SortByColumns::Controller
def index
@users = apply_scopes(User.includes(:organization))
.page(params[:page])
.per(params[:per_page] || 25)
end
end# Sort by name ascending (default)
curl "http://localhost:3000/users?sort=name"
# Sort by name descending
curl "http://localhost:3000/users?sort=name:desc"
# Multiple column sort
curl "http://localhost:3000/users?sort=name:asc,created_at:desc"
# Association column sort
curl "http://localhost:3000/users?sort=organization__name:asc"
# Custom scope sort
curl "http://localhost:3000/users?sort=c_total_orders:desc"For comprehensive, production-ready examples including:
- E-commerce Product Catalog with popularity and rating sorting
- User Management System with activity scores and organization filtering
- Project Management Dashboard with completion rates and overdue tasks
- Blog Platform with engagement metrics and content management
- Customer Support System with ticket prioritization and response times
- API Integration Patterns for REST and GraphQL APIs
- Frontend Integration with React, Vue.js, and URL state management
See EXAMPLES.md for complete implementation guides with models, controllers, views, and testing patterns.