๐Ÿงฉ ConcernsOnRails

๐Ÿ‡ป๐Ÿ‡ณ Note: Hoร ng Sa and Trฦฐแปng Sa belong to Viแป‡t Nam.

A simple collection of reusable Rails concerns to keep your models clean and DRY.

โœจ Features

  • โœ… Sluggable: Generate friendly slugs from a specified field
  • ๐Ÿ”ข Sortable: Sort records based on a field using acts_as_list, with flexible sorting field and direction
  • ๐Ÿ“ค Publishable: Easily manage published/unpublished records using a simple published_at field
  • โŒ SoftDeletable: Soft delete records using a configurable timestamp field (e.g., deleted_at) with automatic scoping
  • ๐Ÿ” Hashable: Auto-generate a random hex/UUID/integer/custom-alphabet value on create, with a regenerate_<field>! helper
  • ๐Ÿ—“๏ธ Schedulable: Manage time-windowed records via starts_at / ends_at with .current, .upcoming, .expired, and .active_at(time) scopes
  • โณ Expirable: Single-timestamp expiry with .active / .expired / .expiring_within(duration) scopes and expire! / extend_expiry! / time_until_expiry helpers

๐Ÿ“ฆ Installation

Add this line to your application's Gemfile:

gem 'concerns_on_rails', github: 'VSN2015/concerns_on_rails'

Then execute:

bundle install

๐Ÿš€ Usage

1. ๐Ÿ“ Sluggable

Add slugs based on a specified attribute.

class Post < ApplicationRecord
  include ConcernsOnRails::Sluggable

  sluggable_by :title
end

post = Post.create!(title: "Hello World")
post.slug # => "hello-world"

If the slug source is changed, the slug will auto-update.


2. ๐Ÿ”ข Sortable

Use for models that need ordering.

class Task < ApplicationRecord
  include ConcernsOnRails::Sortable

  sortable_by :position
end

Task.create!(name: "B")
Task.create!(name: "A")
Task.first.name # => "B" (sorted by position ASC)

You can customize the sort field and direction:

class PriorityTask < ApplicationRecord
  include ConcernsOnRails::Sortable

  sortable_by priority: :desc
end

Additional features:

  • ๐Ÿ“Œ Automatically sets acts_as_list on the configured column
  • ๐Ÿ“‹ Adds default sorting scope to your model
  • โ†•๏ธ Supports custom direction: :asc or :desc
  • ๐Ÿ” Validates that the sortable field exists in the table schema
  • ๐Ÿง  Compatible with scopes and ActiveRecord queries
  • ๐Ÿ”„ Can be reconfigured dynamically within the model using sortable_by

3. ๐Ÿ“ค Publishable

Manage published/unpublished records using a published_at field.

class Article < ApplicationRecord
  include ConcernsOnRails::Publishable
end

Article.published   # => returns only published articles
Article.unpublished # => returns only unpublished articles

article = Article.create!(title: "Draft")
article.published? # => false

article.publish!
article.published? # => true

article.unpublish!
article.published? # => false

Additional features:

  • โœ… published? returns true if published_at is present and in the past
  • ๐Ÿ•’ publish! sets published_at to current time
  • ๐Ÿšซ unpublish! sets published_at to nil
  • ๐Ÿ”Ž Add scopes: .published, .unpublished, and a default scope (optional)
  • ๐Ÿ“ฐ Ideal for blog posts, articles, or any content that toggles visibility
  • ๐Ÿงฉ Lightweight and non-invasive
  • ๐Ÿงช Easy to test and override in custom implementations

4. โŒ SoftDeletable

Soft delete records using a timestamp field (default: deleted_at).

class User < ApplicationRecord
  include ConcernsOnRails::SoftDeletable

  # Optional: customize field and touch behavior
  soft_deletable_by :deleted_at, touch: true
end

Scopes

User.without_deleted   # => returns only active users
User.soft_deleted      # => returns soft-deleted users
User.active            # => same as without_deleted
User.all               # => returns only non-deleted by default (default_scope applied)

Soft delete and restore

user.soft_delete!      # Soft delete the user (sets deleted_at)
user.deleted?          # => true
user.soft_deleted?     # => true (alias)
user.is_soft_deleted?  # => true (alias)

user.restore!          # Restore the user (sets deleted_at to nil)
user.deleted?          # => false

Permanently delete

user.really_delete!    # Hard delete the record from DB

Soft delete/hard delete all records

User.destroy_all           # Soft delete all users (sets deleted_at)
User.really_destroy_all    # Hard delete ALL users (removes from DB)

Callbacks (Hooks)

You can use the following hooks to run logic before/after soft delete or restore:

class User < ApplicationRecord
  include ConcernsOnRails::SoftDeletable

  def before_soft_delete
    # Code to run before soft delete
  end

  def after_soft_delete
    # Code to run after soft delete
  end

  def before_restore
    # Code to run before restore
  end

  def after_restore
    # Code to run after restore
  end
end

Notes

  • Default field is deleted_at, can be changed with soft_deletable_by :your_field
  • touch: false to skip updating updated_at when soft deleting/restoring
  • Aliases for deleted?: soft_deleted?, is_soft_deleted?
  • All scopes and methods work seamlessly with ActiveRecord

5. Hashable

Auto-generate a random value (hex, UUID, fixed-digit integer, or custom-alphabet string) on create.

class Order < ApplicationRecord
  include ConcernsOnRails::Hashable

  # Defaults: type: :hex, length: 16 (32-char hex string)
  hashable_by :token
end

order = Order.create!
order.token              # => "a3f7c9b1e2d40859e2f1c9b73d40a857"
order.regenerate_token!  # rolls a new random value and persists it

Types

hashable_by :token,       type: :hex,     length: 16
hashable_by :external_id, type: :uuid
hashable_by :code,        type: :integer, length: 6
hashable_by :code,        type: :custom,  length: 8,
            alphabet: "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
Type length means Example output
:hex byte count (output is length * 2 hex chars) "a3f7c9b1e2d40859"
:uuid ignored "550e8400-e29b-41d4-a716-446655440000"
:integer digit count 483921
:custom output length, samples from alphabet: "K7M3PQ9A"

Notes

  • Auto-assigns on before_create only when the field is blank, so callers can still pass an explicit value.
  • A regenerate_<field>! instance method is defined dynamically to match the configured column.
  • No uniqueness retry is built in. For collision-prone configurations (e.g. short integer codes), add a unique index and rescue at the application level.
  • For fixed-width numeric codes (e.g. 000042), use a string column โ€” integer columns drop leading zeros.
  • If your model has validates :<field>, presence: true, switch to a before_validation callback in your model since the concern uses before_create.

6. ๐Ÿ—“๏ธ Schedulable

Manage records with a time window using starts_at / ends_at columns.

class Promotion < ApplicationRecord
  include ConcernsOnRails::Schedulable

  # Defaults: starts_at: :starts_at, ends_at: :ends_at
  schedulable_by
end

promo = Promotion.create!(starts_at: 1.hour.ago, ends_at: 1.day.from_now)
promo.current?   # => true
promo.upcoming?  # => false
promo.expired?   # => false

Promotion.current                  # currently active
Promotion.upcoming                 # starts_at in the future
Promotion.expired                  # ends_at in the past
Promotion.active_at(Time.zone.now) # active at any specific time

Custom column names

class Event < ApplicationRecord
  include ConcernsOnRails::Schedulable

  schedulable_by starts_at: :starts_on, ends_at: :ends_on
end

Open-ended start (only an expiry)

class Coupon < ApplicationRecord
  include ConcernsOnRails::Schedulable

  schedulable_by starts_at: nil, ends_at: :expires_at
end

Mutators

promo.start!                                              # sets starts_at to now
promo.finish!                                             # sets ends_at to now
promo.reschedule!(starts_at: 1.day.from_now,
                  ends_at: 2.days.from_now)               # sets both

Notes

  • Boundary semantics are inclusive start, exclusive end: a record is active at exactly starts_at, but not at exactly ends_at.
  • A nil ends_at means "no end" โ€” the record stays active forever once started.
  • A nil starts_at means "not yet started" โ€” the record is not active (unless starts_at is unconfigured).
  • No default_scope is added; chain .current (or any other scope) explicitly to filter.
  • schedulable_by validates that the configured columns exist and raises ArgumentError otherwise.

7. โณ Expirable

For records with a single expiry timestamp โ€” auth tokens, API keys, sessions, password-reset links, invitations.

class ApiToken < ApplicationRecord
  include ConcernsOnRails::Expirable

  # Default field: :expires_at
  expirable_by
end

token = ApiToken.create!(expires_at: 1.hour.from_now)
token.active?               # => true
token.expired?              # => false
token.time_until_expiry     # => ActiveSupport::Duration (~1.hour)

ApiToken.active                       # nil expiry OR future expiry
ApiToken.expired                      # past expiry
ApiToken.expiring_within(1.day)       # future expiry within the next 1.day

Mutators

token.expire!                       # sets expires_at to now
token.expire!(2.hours.from_now)     # sets to an explicit time
token.extend_expiry!(by: 1.day)     # pushes expiry forward

extend_expiry! is "smart" about the base:

  • If expires_at is nil or already in the past, the new value is now + by.
  • If expires_at is still in the future, by is added to the existing value.

Custom field name

class License < ApplicationRecord
  include ConcernsOnRails::Expirable

  expirable_by :valid_until
end

Notes

  • nil expires_at means never expires (the record stays active?).
  • The expiry boundary is exclusive: at exactly expires_at, the record is expired?.
  • No default_scope is added; chain .active explicitly to filter.
  • Expirable overlaps with Schedulable's open-ended pattern (schedulable_by starts_at: nil, ends_at: :expires_at). Use Expirable when the API ergonomics around expiry โ€” active?, expire!, extend_expiry!, expiring_within โ€” fit your domain better; use Schedulable when you also need a start time.

๐Ÿ› ๏ธ Development

To build the gem:

gem build concerns_on_rails.gemspec

To install locally:

gem install ./concerns_on_rails-1.0.0.gem

๐Ÿค Contributing

Bug reports and pull requests are welcome!


๐Ÿ“„ License

This project is licensed under the MIT License.


๐Ÿ‡ป๐Ÿ‡ณ Hoร ng Sa and Trฦฐแปng Sa belong to Viแป‡t Nam.


๐Ÿ”— Source Code

The source code is available on GitHub:

๐Ÿ‘‰ https://github.com/VSN2015/concerns_on_rails

Feel free to star โญ๏ธ, fork ๐Ÿด, or contribute with issues and PRs.