Rails - Using the Attributes API to manage serialized preferences
Tags: rails, ruby
Published: 2020-08-20 14:45:07

I'll admit it I'm not a 'good' coder. I was never employed as a programmer, but I did managed development projects as `functional analysts`. I often got upset if the programmers didn't understand the specs (usually with good reason!) so I'd try to clarify the specs with `steve code`, which was usually a hack! 

I've used many hacks over the years for various reasons (e.g., I was not very familiar with a new language, I was not sure how I would implement what I was trying to do, etc). One of my favorite hacks is to use a serialized hash to define preferences/setting. The goal was to use the hack in development and then add the preferences to columns before production. I often did not accomplish that goal and just used the hash.

Preferences are usually data that seldom get changed, does not need to be queried etc. They may be used as an argument to a calculation, define how something is displayed, etc. I'll use a model `Group` to explain how this is used. The `Group` in my example of a Golf group that has many `Players` and `Games`. The purpose of the application is to compute a quota for each player (similar to handicaps). I have a number of Clubs/Groups that use the application to manage their information. The group has options that controll how these quotas are computed. They can use a different number of `Rounds` and other options in the computation. They may throw out the high and low score in the computation. They may limit the score used in the quota computation for a new player.

My original scheme was to use three hashes in the process. One defined the default keys and values. One defined the casting (string, integer, boolean, etc) of each key/value and one, which is the serialized hash, merges and casts the input params. The default hash (scaled down version) may look like:

```ruby
def default_options
  options = {
    limit_new_player:true,
    limit_rounds:3,
    limit_points:2,
    sanitize_first_round:false,
    rounds_used:10,
    truncate_quota:true,
    use_hi_lo_rule:false,
  }.with_indifferent_access   
end
```

The new scheme is to use the attributes API to replace the casting hash. Attributes are added for each key. A scaled down model (relations not present)

```ruby
class Group < ApplicationRecord
  # relations include Club, Players, Game, Round through Game and Users
  after_initialize :set_attributes

  serialize :preferences, Hash

  attribute :limit_new_player, :boolean
  attribute :limit_rounds, :integer
  attribute :limit_points, :integer
  attribute :sanitize_first_round, :boolean
  attribute :truncate_quota, :boolean
  attribute :rounds_used, :integer
  attribute :use_hi_lo_rule, :boolean

  def set_attributes
    set_default_options if self.preferences.blank?
    self.default_options.each do |k,v|
      #TODO use self.has_attribute?(k) to raise exception if an attribute is not defined, but testing should catch it
      self.send("#{k.to_s}=", self.preferences[k])
    end
  end

  def set_preferences
    self.default_options.each do |k,v|
      self.preferences[k] = self.send(k) 
    end
  end

  def set_default_options
    # set preferences to the default option if preferences blank (a new record)
    self.preferences = self.default_options.to_h
  end

  def default_options
    options = {
      limit_new_player:true,
      limit_rounds:3,
      limit_points:2,
      sanitize_first_round:false,
      rounds_used:10,
      truncate_quota:true,
      use_hi_lo_rule:false,
    }.with_indifferent_access   
  end

  def update_group(params)
    #called from the update method in the Groups controller, all attributes are in the group_params
    self.assign_attributes(params)
    # updates record without saving, just sets attributes
    self.set_preferences
    # now use the set attributes to update to serialized preferences and save
    self.save 
  end

end
```

This may be still be a hack, but a lot cleaner one than my original scheme. The process if fairly simple

* On initialize the serialized preferences are moved to attributes
* Preferences are just like model attributes and used in computations/views (Current.group.rounds_used)
* Forms don't have to use xx_tags helpers 
* You don't have to use Permit on hash for strong params
* Updating preferences acts like real attributes and the attributes are just moved to the serialized hash

I currently have 27 attributes in the current system. Beside the ones used in this post there are attributes that controller the scoring of a game, which team/player(s) won. Golf is a game of skill and games are really mini-tournaments where you pay and entry fee and you may get your money back (or more) if you win.