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