Skip to content

Repository files navigation

Sanitization

Strip, collapse, nullify, and case-fix string attributes in ActiveRecord models without writing a normalizes lambda for every column.

SpecsGem Version

What it does

Sanitization keeps the strings you store in your database clean, without sprinkling normalization rules across every model. It can:

  • Strip leading and trailing whitespace (including unicode invisible characters)
  • Collapse runs of consecutive spaces into a single space
  • Convert empty strings to nil (when the column allows null)
  • Apply case transformations (:up, :down, :title, or any custom *case method)

You declare your intent once, at the model level, and Sanitization figures out which columns to touch by introspecting the schema.

Why not just use normalizes?

As of Rails 7.1, ActiveRecord ships normalizes, which Sanitization is now built on top of (since 2.0). So why use this gem?

Because normalizes is a low-level primitive. It normalizes one attribute at a time, with one lambda at a time, and has no notion of project-wide defaults. In a real app with dozens of models and hundreds of string columns, that gets verbose fast.

Compare a typical model with manual normalizes:

classPerson < ApplicationRecordnormalizes:first_name,:last_name,:email,:phone,:address,:city,with: ->(v){v=v.strip.squeeze(" ")v.empty? ? nil : v}normalizes:email,with: ->(v){v&.downcase}normalizes:first_name,:last_name,with: ->(v){v&.titlecase}# ...and you have to remember to add every new string column here, foreverend

Versus the same thing with Sanitization:

classPerson < ApplicationRecordsanitizes# strip + collapse + nullify on every string columnsanitizesonly: :email,case: :downsanitizesonly: [:first_name,:last_name],case: :titleend

The wins:

  • Sane defaults, set once. Configure strip, collapse, nullify, and case globally in an initializer and every model picks them up. No per-column boilerplate.
  • Schema-aware.sanitizes (with no arguments) automatically applies to every string column on the table. Add a new column, it's covered. No code changes required.
  • Composable rules. Stack multiple sanitizes calls to start from defaults and refine specific columns (only:, except:, per-column case:, etc.) without duplicating the base logic.
  • Smart nullify. Empty strings only become nil when the column is actually nullable, by inspecting null on the column definition. You don't have to remember which columns are NOT NULL.
  • Multibyte-safe whitespace handling. Strips Unicode invisibles (zero-width space, BOM, word joiner, etc.) — the kind of garbage that shows up when users paste from Word or chat apps.

Under the hood, every option you pass is compiled into a single normalizes lambda per column, so you keep all the upstream behavior (assignment-time normalization, where/find_by lookup normalization, record.normalize_attribute(:col), etc.) — you just don't have to write it.

Setting sane defaults

Out of the box, Sanitization does nothing — every option is opt-in. The recommended setup is to enable the common-sense defaults in an initializer:

# config/initializers/sanitization.rbSanitization.configuredo |config|
config.strip=trueconfig.collapse=trueconfig.nullify=trueend# or, equivalently:Sanitization.simple_defaults!

With that in place, calling sanitizes in any model strips, collapses, and nullifies every string column with no further configuration. Models can still override any option locally.

Installation

bundle add sanitization

Compatibility: Ruby >= 3.2, Rails >= 7.1.

Usage

# Assuming the initializer above is loaded.# Apply default settings to every string column.classPerson < ApplicationRecordsanitizes# equivalent to: sanitizes strip: true, collapse: true, nullify: true, include_text_type: falseend# Apply defaults to every string column except one.classPerson < ApplicationRecordsanitizesexcept: :aliasend# Apply defaults, plus titlecase on specific columns.classPerson < ApplicationRecordsanitizesonly: [:first_name,:last_name],case: :titleend# Stack multiple calls to refine behavior per column.classPerson < ApplicationRecord# Defaults + titlecase for every string column except `description`.sanitizescase: :title,except: :description# Override case for two columns.sanitizesonly: [:first_name,:last_name],case: :up# Override case for a single column.sanitizesonly: :email,case: :downcase# Include a `text` column (text columns are skipped by default).sanitizesonly: :description,include_text_type: true# Disable collapsing for one column.sanitizesonly: :do_not_collapse,collapse: false# Use a custom `*case` method (e.g. a String#leetcase you defined),# and don't nullify empty strings on this column.sanitizesonly: :handle,case: :leet,nullify: falseend

Configuration options

OptionValuesDescription
striptrue / falseStrip leading and trailing whitespace (including Unicode invisibles).
collapsetrue / falseCollapse runs of consecutive whitespace into a single space.
nullifytrue / falseConvert empty strings to nil when the column allows null.
case:none, :up, :down, :title, …Apply any String#*case method (built-in or custom).
onlySymbol or ArrayRestrict to specific columns.
exceptSymbol or ArrayExclude specific columns.
include_text_typetrue / falseAlso sanitize text columns (skipped by default).

Behavior notes (built on normalizes)

Since Sanitization 2.0 is implemented on top of Rails' normalizes, normalization runs at attribute assignment time, not in a before_save callback. A few useful consequences:

  • Read-after-write returns the normalized value.person.name = " john "; person.name returns "john" immediately. Validations see the cleaned value.
  • where / find_by hash conditions normalize the lookup value.User.where(email: " RAW@X.COM ") matches a row stored as "raw@x.com". Raw-SQL conditions (where("email = ?", …)) still bypass normalization.
  • Legacy un-normalized rows are not silently rewritten on save. To migrate old data, reassign the attribute or call record.normalize_attribute(:col) (a built-in helper from normalizes).

Upgrading from 1.x? See the CHANGELOG for the full list of behavior changes.

Development

After checking out the repo, run bin/setup to install dependencies. Then run rake spec to run the tests, or bin/console for an interactive prompt.

To run tests across all supported Rails versions:

bundle exec appraisal install
bundle exec appraisal rspec

License

The gem is available as open source under the terms of the MIT License.

About

Clean & sanitize your ActiveRecord data before saving to database

Resources

Stars

17 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages