Alan Coromano
Alan Coromano

Reputation: 26048

Treat a model attribute as a symbol

My model has a string field (a short) and it stores it in db which is fine. But I want it always to return a symbol instead of string and, also, I'd like to assign a symbol to this string attribute. What I'm doing now doesn't work.

class MyModel < ActiveRecord::Base
  attr_accessible :attr1

  def attr1
    # self.try(:attr1).to_sym # how to return symbol?
  end

  def attr1= value
   #    super.attr1.to_sym # doesn't work either
  end
end

How do I reach this?

Upvotes: 2

Views: 3563

Answers (3)

SMAG
SMAG

Reputation: 798

I think there is another approach using ActiveRecord's attribute method

First we need to make a custom ActiveRecord::Type, in this case inheriting from ActiveModel::Type::String. This meets the recommendation from the documentation to extend existing types / inherit from ActiveModel::Type::Value.

# app/lib/core_ext/active_record/type/symbol.rb

module ActiveRecord
  module Type
    class Symbol < ::ActiveModel::Type::String
      def deserialize(obj)
        super&.to_sym
      end

      # defined by ActiveModel::Type::String and we do not need to override for this example
      # def serialize; end
     end
  end
end

Then we need to register this custom type:

# config/initializers/active_record.rb

# Users may also define their own custom types, as long as they respond to the methods defined on the value type.
# The method deserialize or cast will be called on your type object, with raw input from the database or from your
# controllers. See ActiveModel::Type::Value for the expected API.
# It is recommended that your type objects inherit from an existing type, or from ActiveRecord::Type::Value
# https://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute

require "./app/lib/core_ext/active_record/type/symbol"

ActiveRecord::Type.register(:symbol, ActiveRecord::Type::Symbol)

Now we can set our symbol type as follows:

class MyModel < ApplicationRecord
  attribute :attr1, :symbol
end

Examples

instance = MyModel.new(attr1: "string")

instance            => #<MyModel:0x0000ffffacaa19d0 id: 1, attr1: :string>
instance.attr1      => :string
instance.inspect    => "#<MyModel id: 1, attr1: :string>"
instance.attributes => {"id"=>1, "attr1"=>:string }

# to confirm the value is getting serialized correctly and stored as a string we can look at the before_type_cast method for the attribute
instance.attr1_before_type_cast => "string"
instance.attributes_before_type_cast => {
 "id"=> ...,
 "attr1"=> "address",
 "created_at"=>2024-05-06 23:47:38.85182 UTC,
 "updated_at"=>2024-05-06 23:47:38.85182 UTC
}

Upvotes: 1

KARASZI Istv&#225;n
KARASZI Istv&#225;n

Reputation: 31477

I think you only need to overwrite the getter, the setter probably works fine if it is a field.

class MyModel < ActiveRecord::Base  
  def attr1
    attributes['attr1']&.to_sym
  end
end

Or you could also create a Serializer:

class SymbolSerializer
  def self.dump(obj)
    return unless obj
    obj.to_s
  end

  def self.load(text)
    return unless text
    text.to_sym
  end
end

And then in your model:

class MyModel < ActiveRecord::Base
  serialize :attr1, SymbolSerializer
end

Upvotes: 9

Vadym Tyemirov
Vadym Tyemirov

Reputation: 8853

If you need to do it on multiple columns or in different models I would suggest to generalize the solution:

class MyModel < ActiveRecord::Base  
  include Concerns::Columnable

  treat_as_symbols :attr1
end

module Concerns::Columnable
  extend ActiveSupport::Concern

  included do
    def self.treat_as_symbols *args
      args.each do |column|
        define_method "#{column}" do
          read_attribute(column.to_sym).to_sym
        end
      end
    end
  end
end

Upvotes: 2

Related Questions