Search code examples
ruby-on-railsrubyclassobjectattr-accessor

How to set different values for setters per active record object in ruby rails?


I am trying to assign session values to model object as below.

 # models/product.rb 

 attr_accessor :selected_currency_id, :selected_currency_rate, :selected_currency_icon

 def initialize(obj = {})
    selected_currency_id = obj[:currency_id]
    selected_currency_rate = obj[:currency_rate] 
    selected_currency_icon = obj[:currency]
 end

but this works only when I initialize new Product object

selected_currency = (session[:currency].present? ? session : Currency.first.attributes)  
Product.new(selected_currency)

While, i need to set these setter methods on each product object automatically even if was fetched from Database.(active record object) ie. Product.all or Product.first

Earlier i was manually assigning values to each product object after retrieving it from db on controller side.

@products.each do |product|
   product.selected_currency_id = session[:currency_id] 
   product.selected_currency_rate = session[:currency_rate] 
   product.selected_currency_icon = session[:currency]
end

But then i need to do it on every method where product details need to be displayed. Please suggest a better alternative to set these setter methods automatically on activerecord objects.


Solution

  • I don't think you really want to do this on the model layer at all. One thing you definitely don't want to do is override the initializer on your model and change its signature and not call super.

    Your model should only really know about its own currency. Displaying the price in another currency should be the concern of another object such as a decorator or a helper method.

    For example a really naive implementation would be:

    class ProductDecorator < SimpleDelegator
      attr_accessor :selected_currency
    
      def initialize(product, **options)
        # Dynamically sets the ivars if a setter exists
        options.each do |k,v|
          self.send "#{k}=", v if self.respond_to? "#{k}="
        end
        super(product) # sets up delegation
      end
    
      def price_in_selected_currency
        "#{ price * selected_currency.rate } #{selected_currency.icon}"
      end
    end 
    
    class Product
      def self.decorate(**options)
        self.map { |product|  product.decorate(options) }
      end
    
      def decorate(**options)
        ProductDecorator.new(self, options)
      end
    end
    

    You would then decorate the model instances in your controller:

    class ProductsController
      before_action :set_selected_currency 
    
      def index
        @products = Product.all
                           .decorate(selected_currency: @selected_currency)
      end
    
      def show
        @product = Product.find(params[:id])
                          .decorate(selected_currency: @selected_currency)
      end
    
      private
        def set_selected_currency 
          @selected_currency = Currency.find(params[:selected_currency_id])
        end
    end
    

    But you don't need to reinvent the wheel, there are numerous implementations of the decorator pattern like Draper and dealing with currency localization is complex and you really want to look at using a library like the money gem to handle the complexity.