Last active
April 21, 2021 22:17
-
-
Save chunpan/fd055d0c11cb0b912b5f69e4ddde743d to your computer and use it in GitHub Desktop.
Ruby Module to Allow Auto Truncation of String Columns using ActiveRecord Callbacks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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