A singleton used for configuration couple with store_accessor

Hey guys today I wanna talk about something I encounter during the project I am working on in my company. We’ll see what is a singleton (as a reminder) and how to implement it with an hstore feature. The advantage of this is having dynamical attributes rather than a migration.

The singleton is a class that can be instantiated only once. This is pretty cool for a logger or configuration for the web. Another use for mobile development is a singleton RESTClient, you can invoke from everywhere and fetch data. Most of the example on the internet are with ruby, this is cool in theory but let’s see how to accomplish this in practice with Rails. We will have one model representing the singleton.

shop_configuration.rb

class ShopConfiguration < FftServerModel
  include ShopConfigHash
  validate :must_be_singleton, on: :create

  private

  def fill_with_default
    self.config_hash =
      self.class.default_config.
      merge((config_hash || {}).delete_if { |_key, value| value.nil? })
  end

  def must_be_singleton
    errors.add_to_base :must_be_singleton if ShopConfiguration.any?
  end

  class << self
    def singleton
      record = first || new
      record.fill_with_default
      record.save! if record.new_record? || record.changed?
      record
    end
  end
end

Here we have a singleton class. We call it through ShopConfiguration.singleton. The validation makes it a singleton : there’s only one class configuration in the project. If we try to create two configurations class, an error will throw. Note that we use add_to_base to add the error.

shop_config_hash.rb

module ShopConfigHash
  include Shipping

  included do
    store_accessor :config_hash, *default_config.keys
  end
end

This is where the magic happen. Basically when you’ll do something like ShopConfiguration.singleton.parcel_preparation_ref_validation_method it will executes the equivalent of ShopConfiguration.singleton.config_hash.parcel_preparation_ref_validation_method. The store_access function is in charge to write the value of our content and read it directly in the attribute passed in parameters, i.e the config_hash. If you take a look on how the store_accessor function is implemented : it defines a setter / getter method and make these function retrieve / set the value from the attribute set as first parameter, i.e config_hash. You should think of store_accessor as just a shortcut when you call any method, instead of searching for this method at the instance level like a regular attribute, it will go and search inside the given attribute.

store.rb

  def store_accessor(store_attribute, *keys)
    keys = keys.flatten

    _store_accessors_module.module_eval do
      keys.each do |key|
        define_method("#{key}=") do |value|
          write_store_attribute(store_attribute, key, value)
        end

        define_method(key) do
          read_store_attribute(store_attribute, key)
        end
      end
    end

One last thing, if you take a look at the snippet below with the splat operator, passing *default_config.keys as parameter is just splitting the array of key into multiple argument. Basically it means this :

def go(x, y)
end

point = [1, 2]
go(*point)

common.rb

module ShopConfigHash::Common
  extend ActiveSupport::Concern

  class_methods do
    attr_reader :default_config

    private

    def key(name, type, default: nil)
      @default_config ||= {}
      @default_config[name] = default

      define_singleton_method(name) do
        singleton.send(name)
      end

      define_method :"#{name}=" do |value|
        clazz = "ActiveModel::Type::#{type.capitalize}".constantize
        super(clazz.new.cast(value))
      end
    end
  end
end

This code is reusing the same principle as store_accessor. All calls like ShopConfiguration.my_attribute will be forwarded to the singleton class method (an instance of ShopConfiguration) When you do ShopConfiguration.parcel_preparation_ref_validation_method it will call ShopConfiguration.singleton.parcel_preparation_ref_validation_method.

If you follow the article, this call is forwarded to the config_hash attribute. When you do ShopConfiguration.parcel_preparation_ref_validation_method as getter or setter you actually call ShopConfiguration.singleton.config_hash.parcel_preparation_ref_validation_method.

One last useful thing to notice is the cast defined in the setter method. What happen is we call on the fly, the ActiveModel::Type::Boolean for example and cast the value.

shipping.rb

module ShopConfigHash::Shipping
  extend ActiveSupport::Concern
  include ShopConfigHash::Common

  included do
    key :parcel_preparation_ref_validation_method, :string

    key :parcel_preparation_ref_validation_strict, :boolean, default: false

    validates :parcel_preparation_ref_validation_strict,
              inclusion: { in: [ true, false ] }
    validates :parcel_preparation_ref_regex,
              presence: true, if: :parcel_prep_ref_regex_validation_method?    
  end

  private

  def parcel_prep_ref_regex_validation_method?
    parcel_preparation_ref_validation_method == 'Regexp'
  end
end

This is the final use of the function where you can hard code your attribute. I found this approach really interesting because it allows to split your attributes per file and defined validation for each of them, no needs to have a migration. It can be really useful when you have a ton of settings you don’t want to use.

Search