Skip to content

Instantly share code, notes, and snippets.

@chunpan
Last active April 21, 2021 22:17
Show Gist options
  • Save chunpan/fd055d0c11cb0b912b5f69e4ddde743d to your computer and use it in GitHub Desktop.
Save chunpan/fd055d0c11cb0b912b5f69e4ddde743d to your computer and use it in GitHub Desktop.
Ruby Module to Allow Auto Truncation of String Columns using ActiveRecord Callbacks
require 'active_support/core_ext/class/attribute'
require 'active_support/concern'
# This module enables a simple declaration for the including ActiveRecord model
# class to specify one or more columns (attributes) to automatically truncate
# its length to a max value or as defined by the column width in your schema.
#
# Usage example:
#
# class MyModel << ActiveRecord::Base
# include Concerns::AutoTruncateStringColumn
# auto_truncate :body, limit: 191 # optional, default to column length in DB
# end
#
# Place this module in your Rails app's `app/models/concerns` directory.
#
# The license to use this code is free, as long as you include the entirety of
# this file, including this comment section with the author information below:
#
# @auth C. Billy Pan (https://gist.github.com/chunpan/fd055d0c11cb0b912b5f69e4ddde743d)
module Concerns
module AutoTruncateStringColumn
extend ActiveSupport::Concern
included do
class_attribute :truncation_sizes
self.truncation_sizes = {}
before_save :truncate_registered_string_columns
end
class_methods do
# The main class method to declare/register string columns for auto-truncation.
def auto_truncate_string_columns(*cols)
options = cols.extract_options!
limit = options[:limit].to_i
self.truncation_sizes ||= {}
cols.inject(self.truncation_sizes) do |memo, col|
col_size = self.columns_hash[col.to_s].limit
size_limit =
if limit <= 0
col_size
else
[limit, col_size].min
end
memo[col.to_sym] = size_limit
memo
end
end
alias_method :auto_truncate, :auto_truncate_string_columns
end
protected
def truncate_registered_string_columns
self.truncation_sizes.each_pair do |col, size_limit|
if self.__send__(col).length > size_limit
self.__send__(:"#{col}=", self.__send__(col)[0..size_limit-1])
end
end
end
end
end
require 'rails_helper'
# sample rspec test of a model class that includes an auto-truncated column
# named `body` and a standard column of `name`, both are defined with max length
# of 191 in a MySQL database.
RSpec.describe MyModel, type: :model do
# assuming that FactoryGirl is used for fixture here
let(:my_model) { create(:my_model, name: 'My Name', body: 'Hi there!') }
# real-life example of text that produced SQL truncation error at
# https://app.honeybadger.io/projects/46944/faults/32244164
let(:long_body) do
'Our company takes the confusion out of digital marketing. We already built you an ad; all you ' \
'have to do is tell us which zip codes you want to advertise in to start reaching more ' \
'potential clients in your area.'
end
let(:truncated_long_body) do
long_body[0..190]
end
# mock a long text for a column that is not registered to auto-truncate
let(:long_name) { long_body }
# tests related to auto-truncation behavior mixed in via a module
describe 'behavior of `Concerns::AutoTruncateStringColumn`' do
before :example do
# expect that the `before_save` hook it provides will get visited
expect(my_model).to receive(:truncate_registered_string_columns).once.and_call_original
end
subject { my_model.save }
context 'when `body` value is below the limit' do
before :example do
my_model.body = 'Hi, there again!'
end
it 'has no effect on `save`' do
expect(subject).to be true
expect(my_model.body).to eq 'Hi, there again!'
end
end
context 'when `body` value is above the limit' do
before :example do
expect(my_model.body).to_not eq truncated_long_body
my_model.body = long_body
end
it 'truncates the `body` value up to the limit' do
expect(subject).to be true
expect(my_model.body).to_not eq long_body
expect(my_model.body).to eq truncated_long_body
end
end
context 'when non-auto-truncate column `name` value is above the limit' do
before :example do
expect(my_model.name).to_not eq long_name
my_model.name = long_name
end
it 'does not provide protection for SQL error' do
expect { subject } .to raise_error(ActiveRecord::StatementInvalid,
/Mysql2::Error: Data too long for column 'name'/)
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment