Skip to content

Unexpected behavior concerning 'default-settings' in class hierarchies (STI) #102

@mayinx

Description

@mayinx

The consecutive use of the has_settings-macro in a class hierarchy along with the persistent: true-option produces (more or less) unexpected behavior concerning the default_settings-hash. Given the following setup you would expect (at least I do) that the User-subclass Student would inherit and redefine/extend its base_class' default_settings - but of course without affecting its base_class' default_settings-hash:

class User < ActiveRecord::Base
  has_settings persistent: true do |s|
    s.key :theme, defaults: { name: 'default', base_size: '14' }
    s.key :calendar, defaults: { scope: 'school' }    
  end
end

class Student < User
  has_settings persistent: true do |s|
    s.key :theme, defaults: { name: "student", base_size: "18"  }  
    s.key :profile, defaults: { scope: 'school_class' }
  end
end

class Teacher < User; end

Problem: As of now, calling default_settings on the subclass Student changes the base-class' default_settings as well - and even the settings of other subclasses (like Teacher in this example):

# alrighty:
User.default_settings:
=> {:theme=>{"name"=>"default", "base_size"=>"14"}, :calendar=>{"scope"=>"school"}}

# all good - no suprises here:
Teacher.default_settings:
=> {:theme=>{"name"=>"default", "base_size"=>"14"}, :calendar=>{"scope"=>"school"}}

# still good? At least looks like it:
Student.default_settings:
=> {:theme=>{"name"=>"student", "base_size"=>"18"}, :calendar=>{"scope"=>"school"},  :profile=>{"scope"=>"school_class"}}

# but: uh - oh - no good:
User.default_settings:
=> {:theme=>{"name"=>"student", "base_size"=>"18"}, :calendar=>{"scope"=>"school"},  :profile=>{"scope"=>"school_class"}}

# oh dear:
Teacher.default_settings:
=> {:theme=>{"name"=>"student", "base_size"=>"18"}, :calendar=>{"scope"=>"school"},  :profile=>{"scope"=>"school_class"}}

This is due to the internal implementation of the default_settings-hash as class_attribute and a (at least in an inheritance-setting) 'suboptimal' value assignment (via @klass.default_settings ||= {}) on this mutable structure - see RailsSettings::Configuration#initialize:

@klass.default_settings ||= {}

Are there any plans on addressing STI-related issues like this in future releases?

Anyway, for the meantime I came up with the following workaround - at least for my use case it seems to do the job just fine - hopefully, this helps others with the same issue:

# Inheritance-Patch (applied via mixin prepending, see down below) to handle
# consecutive uses of the 'has_settings'-macro along with the 'persistent: true'-option  
# in a class hierarchy.
#
# Ensures correct initializations of the 'default_settings'-class_attribute
# in subclasses by extending/redefining RailsSettings::Base.included; adds
# an 'inherited'-hook to the included behavior which clones the parent's mutable
# class_attribute 'default_settings' when subclassed.
#
# FYI: Necessary when using mutable structures likes hashes or arrays as
# class_attribute - otherwise setting the subclasses 'default_settings' would
# effect the base class' 'default_settings' as well (operation on the same
# element etc.). See here for more details:
#
#   https://apidock.com/rails/Class/class_attribute
#
module RailsSettingsBaseStiExtension
  def included(base)
    super(base) # call the default implementation first

    # Add 'inherited'-hook to clone 'default_settings' from parent to child class
    base.define_singleton_method(:inherited) do |subClass|
      if (self.methods.include?(:default_settings) &&
          subClass.methods.include?(:default_settings))
        subClass.default_settings = self.default_settings.clone
      end

      super(subClass) # necessary to avoid trouble / to silence warnings
    end
  end
end

# Use 'prepend' to be able to call 'super' in the extension to keep
# the default behavior and just add our extension to it
RailsSettings::Base.singleton_class.send(:prepend, RailsSettingsBaseStiExtension)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions