rails
rails copied to clipboard
ActiveRecord::Normalization - Normalizations don't run on store-based attributes
Steps to reproduce
# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "rails", github: "rails/rails", branch: "main"
gem "sqlite3"
end
require "active_record"
require "minitest/autorun"
require "logger"
# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :users, force: true do |t|
t.string :extra_data
end
end
class User < ActiveRecord::Base
store :extra_data, accessors: [:fav_color, :homepage], coder: JSON
store_accessor :extra_data, :email
# Normalizations on store attributes DO NOT work
normalizes :homepage, :email, with: -> { _1.strip.downcase }
# Validations on store attributes DO work
validates :fav_color, inclusion: {in: %w[red green blue]}, allow_nil: true
end
class BugTest < Minitest::Test
def test_association_stuff
user = User.create(homepage: " http://Example.Com ")
user.fav_color = "red"
user.email = "[email protected]"
user.save! # no errors, color validation works
assert_equal "http://example.com", user.homepage
assert_equal "[email protected]", user.email
end
end
Expected behavior
Both the homepage and email attributes should be normalized (stripped and downcased) when set. This is expected as validations work on store-based attributes.
Actual behavior
Normalizations don't appear to be running on store-based attributes. Not sure if this is a bug, an oversight, or intended behavior.
System configuration
Rails version: 7.1.1
Ruby version: 3.2.2
Looking at the implementation, I am not sure there would be a simple solution to this problem, because accessors added via store's :accessors options are not "classic" attributes, compared to db-backed attribute or attributes defined via attribute method. normalizes does not work for attr_accessor defined attributes too.
You can solve your problem by something like:
normalizes :extra_data, with: ->(data) {
data["homepage"] = data["homepage"].strip.downcase if data["homepage"]
data["email"] = data["email"].strip.downcase if data["email"]
data
}
Probably we should extend the docs for this case?
cc @jonathanhefner (as implementor of this feature https://github.com/rails/rails/pull/43945)
@fatkodima is correct. normalizes "declares a normalization for one or more attributes", and store accessors are not implemented as attributes. However, as he mentions, the store itself is an attribute, so you can normalize it like in his example. If all of the stores members need the same normalization, you may be able to shorten the code a bit:
normalizes :extra_data, with: -> store { store.transform_values { _1.strip.downcase } }
Normalizations are implemented as attribute type decorations, so for normalizations to apply to store accessors, we would need to hook the accessors into the attribute system somehow. I haven't really thought this through, but if I were implementing the API from scratch, I would probably invert the relationship between "store accessors" and the store such that a store attribute is actually compiled from other attributes. For example, something like:
class User < ActiveRecord::Base
attribute :fav_color, :string
attribute :homepage, :string
store :extra_data, from: [:fav_color, :homepage], coder: JSON
end
I'm not sure how feasible that would be though.
Could this pain be eased, if ActiveRecord::Normalization were a ActiveModel::Normalization because then one could work around this by converting a store attribute to an ActiveModel-based serializer.