Module: ConcernsOnRails::Models::Storable

Extended by:
ActiveSupport::Concern
Defined in:
lib/concerns_on_rails/models/storable.rb

Overview

Typed, defaulted, optionally-validated accessors over a single JSON (or serialized-text) column (“store_attribute-lite”). Rails’ native ‘store_accessor` is untyped on every supported version (a form-submitted “true” stays the String “true”), ships no defaults, and exposes no per-key dirty methods — the gap that the store_attribute / jsonb_accessor gems exist to fill. This concern closes it with no extra dependency.

class Account < ApplicationRecord
  include ConcernsOnRails::Storable

  storable_by :settings,
    theme:          { type: :string,  default: "light", in: %w[light dark] },
    notifications:  { type: :boolean, default: true },
    items_per_page: { type: :integer, default: 25 },
    trial_ends_at:  { type: :datetime }
  storable_by :flags, { beta: { type: :boolean, default: false } }, prefix: :flag
end

.theme              # => "light" (virtual default; nothing stored yet)
.notifications = "0"
.notifications      # => false   (cast, not the String "0")
.notifications?     # => false   (boolean keys get a predicate)
.flag_beta          # => false   (prefixed accessor)
.items_per_page_changed?  # per-key dirty, computed off the column's _was
.reset_theme        # drop the key so the reader falls back to the default

Per key: ‘type:` (:string default, :integer, :float, :decimal, :boolean, :date, :datetime, :json), `default:` (a value, or a Proc instance_exec’d per read), ‘in:` (an enumerable membership set). The macro is repeatable —repeat calls for the SAME column merge keys; different columns are independent. `prefix:`/`suffix:` rename the generated accessors as `<prefix>_<key>_<suffix>`.

Notes:

* Whole-column dirty: writing one key reassigns (and so dirties) the
  entire column. Two requests writing different keys of the same row are
  last-write-wins on the whole hash — there is no per-key merge on save.
* nil vs unset: a writer-stored nil (explicit JSON null) reads back as
  nil and does NOT fall back to the default; `reset_<key>` removes the
  key entirely so the reader resolves the default again.
* :json values are passed through uncast and the reader returns a dup —
  reassign (`record.config = record.config.merge("k" => 1)`), don't
  mutate in place, or the write is silently lost.
* Read-side casting never raises: corrupt column JSON decodes to {} and
  ungarbageable values cast to nil (ActiveModel semantics). :decimal is
  stored precision-safe as a String (BigDecimal), :date/:datetime as
  ISO8601 strings (datetime in UTC, microsecond precision).
* Reserved option names: passing key specs as keyword arguments means a
  key literally named `prefix` or `suffix` would be swallowed by the
  affix options — declare those via the positional Hash escape hatch
  (`storable_by :col, { prefix: { type: :string } }`).
* Reach for the store_attribute or jsonb_accessor gems when you need
  querying into the store, jsonb operators, or store-backed scopes.

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

LABEL =
"ConcernsOnRails::Models::Storable".freeze
VALID_TYPES =
%i[string integer float decimal boolean date datetime json].freeze
ALLOWED_SPEC_KEYS =
%i[type default in].freeze
CASTERS =

Reusable ActiveModel casters for the JSON-native types. :decimal, :date and :datetime round-trip through Strings and are handled explicitly; :json is passed through uncast.

{
  string: ActiveModel::Type::String.new,
  integer: ActiveModel::Type::Integer.new,
  float: ActiveModel::Type::Float.new,
  boolean: ActiveModel::Type::Boolean.new,
  date: ActiveModel::Type::Date.new,
  datetime: ActiveModel::Type::DateTime.new
}.freeze