module CushionDefaults::ClassMethods

A series of class methods to be plopped into any class that includes CushionDefaults.

Attributes

defaults[R]

Reader for the @defaults@ @DefaultsHash@.

descendents[R]

Reader for the @defaults@ @DefaultsHash@.

Public Instance Methods

bang_reader(*syms) click to toggle source

Identical to {#cushion_reader} with one important exception: after determining the default value, @bang_reader@ goes on to crystallize this default value for the instance.

This is especially useful with proc cushions, as the below examples make clear.

Note that this method is equivalent to calling @instance.crystallize_default(:sym)@.

Note also, finally, that bang_readers can exist and function even if no {#cushion_writer}s are defined.

@param syms [*#to_sym] instance variables that should have @bang_readers@ @see CushionDefaults::Configuration#bang_things_up Configuration#bang_things_up @see CushionDefaults::CushionDefaults#crystalize_default CushionDefaults#crystalize_default

@example Simple Example #1: Flowers for cushion_reader

class Flower
    include CushionDefaults
    self.defaults[:he_loves_me] = proc { (Time.now.sec % 2).zero? }
    cushion :he_loves_me
end

daisy = Flower.new

daisy.he_loves_me  # true

# slight delay...
daisy.he_loves_me  # false

# slight delay...
daisy.he_loves_me! # true

5.times { daisy.he_loves_me } # [true, true, true, true, true]

@example Simple Example #2: Wake Up Little Susie

class Dreamer
    include CushionDefaults
    self.defaults[:time_awoken] = proc { Time.now }
    cushion :time_awoken
 end

little_susie = Dreamer.new

little_susie.time_awoken  # returns Time.now, but doesn't set @time_awoken
little_susie.time_awoken! # sets @time_awoken to Time.now

little_susie.time_awoken  # returns the @time_awoken set in the line above
little_susie.time_awoken! # returns @time_awoken and does not set it to a new value
# File lib/cushion_defaults/class_methods.rb, line 227
def bang_reader(*syms)
  syms.each do |sym|
    sym = sym.to_sym
    bang_sym = "#{sym}!".to_sym
    if self_or_parent_instance_method?(bang_sym)
      CushionDefaults.log("#{self} or a parent class already has a bang method #{bang_sym}", :warn)
    end
    define_method(bang_sym) do
      crystallize_default(sym)
    end
    CushionDefaults.log("bang_reader #{bang_sym} established for #{self}")
  end
end
cushion(*syms) click to toggle source

Sets up both {#cushion_reader}s and {#cushion_writer}s for @syms@. Will set up {#bang_reader}s if {CushionDefaults::Configuration#bang_things_up Configuration#bang_things_up} is true. @param syms [*#to_sym] Those instance variables that should have {#cushion_reader}s and {#cushion_writer}s.

# File lib/cushion_defaults/class_methods.rb, line 380
def cushion(*syms)
  cushion_reader(*syms)

  cushion_writer(*syms)
end
cushion_defaults() click to toggle source

Defines {#cushion_reader}s and {#cushion_writer}s for all of this class’ @defaults@.

Only defines @cushion_readers@ and @cushion_writers@ for this class defaults. All other getters are assumed to have been defined further up the class tree.

Thus, if class @A@ defines the default @var1@, and class @B@ defines the default @var2@, calling this method within class @B@ will only generate a getter for @var2@.

See also:

# File lib/cushion_defaults/class_methods.rb, line 408
def cushion_defaults
  cushion *defaults.keys
  CushionDefaults.log("cushions established for #{self}'s defaults': #{defaults.keys.join(', ')}", :info)
end
cushion_reader(*syms) click to toggle source

Sets up a cushion_reader for each :sym in @syms@.

Each reader method checks if its instance variable (:sym) is defined. If it is, it returns that. If not, it returns the default value.

The only exception to this rule is when we have pushy defaults. If a default is pushy, we return it regardless of what this object’s instance variable may or may not be.

Note that if the default responds to :call (a proc, e.g.), then the default is instead called with the instance variable and the symbolic representation of the default requested. This allows for proc cushions.

Will set up {#bang_reader}s if {CushionDefaults::Configuration#bang_things_up Configuration#bang_things_up} is true.

The readers are named according to the same format as @attr_reader@. @param syms [*#to_sym] instance variables that should have @cushion_readers@ @see Configuration#update_readers

# File lib/cushion_defaults/class_methods.rb, line 106
def cushion_reader(*syms)
  # send it with force==true
  syms.each { |sym| _update_cushion_reader(sym.to_sym, true) }
  bang_reader *syms if CushionDefaults.conf.bang_things_up
end
cushion_readers_for_defaults() click to toggle source

Defines {#cushion_reader}s for all of this class’ @defaults@.

Only defines @cushion_readers@ for defaults specified for this class. All other readers are assumed to have been defined further up the class tree.

Thus, if class @A@ defines the default @var1@, and class @B@ defines the default @var2@, calling this method within class @B@ will only generate a reader for @var2@.

# File lib/cushion_defaults/class_methods.rb, line 393
def cushion_readers_for_defaults
  cushion_reader *defaults.keys
end
cushion_writer(*syms) click to toggle source

Sets up a cushion_writer for each :sym in @syms@.

Each writer method carries out one of several checks.

First, it sees if the default is nil or an empty string, and if we care whether this is the case. If {Configuration#ignore_attempts_to_set_nil} is true, we will not set the instance variable to nil; and correspondingly, if {Configuration#blank_str_is_nil} is true, we will not set the instance variable to ”. This failure will be logged at the debug level unless {Configuration#whiny_ignores}, in which case it will be logged at warn.

Assuming the above checks pass, we next check whether the key we’re interested in is pushy. If it is, it warns us that the new value will have no effect since it will be overridden by a pushy default.

Finally, assuming it was not flagged as nilish, the variable is set to the new value.

The writers are named according to the same format as @attr_writer@. @param syms [*#to_sym] instance variables that should have @cushion_writers@ @see Configuration#update_writers @see Configuration#ignore_attempts_to_set_nil @see Configuration#blank_str_is_nil @see Configuration#whiny_ignores

# File lib/cushion_defaults/class_methods.rb, line 261
def cushion_writer(*syms)
  syms.each do |sym|
    method_name = "#{sym}=".to_sym
    if self_or_parent_instance_method?(method_name)
      CushionDefaults.log("#{self} or a parent class already has what looks like a setter method for #{sym}", :warn)
    end

    instance_variable_sym = "@#{sym}".to_sym

    defaults_cache = @defaults

    define_method(method_name) do |y|
      if CushionDefaults.nilish? y
        if CushionDefaults.conf.whiny_ignores
          CushionDefaults.log("You are attempting to set a nilish value for #{sym}. This will not be recorded, and any value set will be deleted.", :warn)
        end
      else
        if CushionDefaults.conf.we_have_a_pushy? && defaults_cache.pushy?(sym)
          CushionDefaults.log("You are setting a value for #{sym}, but this is a pushy default and this value will not be returned by any cushion_readers.", :warn)
        end
        instance_variable_set(instance_variable_sym, y)
      end
    end
    CushionDefaults.log("cushion_writer #{sym}= established for #{self}")
  end
end
deep_freeze_default(*syms) click to toggle source

In addition to preventing the @default@ from being set to a new value (see {#freeze_default}), also freezes the value to which @default@ is currently set.

Does not throw an error when you attempt to freeze an already-frozen default, but it does log at warning level.

Equivalent to calling: @freeze_default :sym; defaults.freeze@

@api freeze

# File lib/cushion_defaults/class_methods.rb, line 451
def deep_freeze_default(*syms)
  syms.each do |sym|
    sym = sym.to_sym
    freeze_default sym
    defaults[sym].freeze
  end
end
deep_freeze_defaults() click to toggle source

Calls {#deep_freeze_default} for all defaults.

@api freeze

# File lib/cushion_defaults/class_methods.rb, line 462
def deep_freeze_defaults
  deep_freeze_default *defaults.keys
end
defaults=(replacement_hash) click to toggle source

@!attribute [w] defaults Wipe @defaults and replace it with the keys/vals of @replacement_hash@.

If you only want to add one or more defaults, then write instead self.defaults += {new_key: val}.

Note that all keys are coerced into @Symbols@.

# File lib/cushion_defaults/class_methods.rb, line 16
def defaults=(replacement_hash)
  # Need to copy over keys/vals to ensure @defaults remains a DefaultsHash and retains identity

  CushionDefaults.log("Old defaults deleted for #{self}:#{defaults.empty? ? ' [none]' : @defaults.reduce(''){|s,(k,v)| s+"\n\t\t#{k}: #{v}"}}", :info) unless @defaults.empty?
  @defaults.replace(replacement_hash)
  CushionDefaults.log("New defaults added for #{self}:#{defaults.empty? ? ' [none]' : @defaults.reduce(''){|s,(k,v)| s+"\n\t\t#{k}: #{v}"}}", :info)
  @defaults.keys.each do |key|
    unless key.is_a? Symbol
      @defaults[key.to_sym] = @defaults[key].delete!
    end
  end
end
defaults_from_yaml(file_name = nil) click to toggle source

Load in the defaults for this class from a YAML file. If @file_name@ is specified, this YAML file is loaded. Otherwise, {Configuration#yaml_file_for} is evaluated for the current class. By default, the yaml file for @Klass@ is expected to be at @config/cushion_defaults/klass.yaml@. @param file_name [String] name of the YAML file to be loaded in for this class @see Configuration#auto_load_from_yaml @see Configuration#whiny_yaml

# File lib/cushion_defaults/class_methods.rb, line 44
def defaults_from_yaml(file_name = nil)
  if file_name
    yaml_path = "#{CushionDefaults.conf.yaml_source_full_path}#{file_name}.yaml"
  else
    yaml_path = CushionDefaults.conf.yaml_file_for(self)
  end

  yaml = YAML::load(File.open(yaml_path)) rescue {}

  if yaml.empty?
    CushionDefaults.log("No YAML configuration for class #{self} found at #{yaml_path}", CushionDefaults.conf.whiny_yaml ? :warn : :debug)
  end

  initialize_defaults_hash
  log_str = "New defaults added for #{self}:#{' [none]' if yaml.empty?}"
  # If automatic readers and writers are enabled, this will set them up as a consequence.
  yaml.each do |key, val|
    log_str << "\n\t\t#{key}: #{val}"
    @defaults[key.to_sym] = val
  end
  CushionDefaults.log(log_str, :info)
end
freeze_default(*syms) click to toggle source

Prevents a default from being set to a new value. Freezing a default is permanent in the life of the program.

Note that while a frozen default is guaranteed to maintain its identity, its attributes can still be modified (e.g., by bang! methods). To prevent any modification to a default value, call {#deep_freeze_default}.

Does not throw an error when you attempt to freeze an already-frozen default, but it does log at warning level.

@note A frozen default can still be overridden lower in the class hierarchy. @see thaw_default(sym)

@api freeze

# File lib/cushion_defaults/class_methods.rb, line 426
def freeze_default(*syms)
  syms.each do |sym|
    sym = sym.to_sym
    unless defaults.freeze_default! sym
      # It returns nil if the key was already present in the Set
      CushionDefaults.log("Cannot freeze #{sym}: it was already frozen for #{self}", :warn)
    end
  end
end
freeze_defaults() click to toggle source

Calls {#freeze_default} for all defaults.

@api freeze

# File lib/cushion_defaults/class_methods.rb, line 439
def freeze_defaults
  freeze_default *defaults.keys
end
inherited(inheritor) click to toggle source

Ensure that if class @Klass@ includes @CushionDefaults@, then any class that subclasses @Klass@ will include it as well.

Called automatically whenever any class that includes @CushionDefaults@ is inherited. @api private

Calls superclass method
# File lib/cushion_defaults/class_methods.rb, line 557
def inherited(inheritor)
  inheritor.send :include, CushionDefaults
  @descendents << inheritor
  super inheritor
end
initialize_defaults_hash() click to toggle source

Either set up or wipe @defaults. Should not usually be called directly. @api private

# File lib/cushion_defaults/class_methods.rb, line 69
def initialize_defaults_hash
  if @defaults
    # We need to maintain the identity of the hash, as child classes may have stored a reference to it
    @defaults.clear
  else
    @defaults = DefaultsHash.new(self)
  end
end
make_polite(*syms) click to toggle source

Declare each sym to be polite. A polite default operates as normal. This is useful to override an ancestor class’ pushy declaration.

This will automatically create {#cushion_reader}‘s for all variables made polite.

# File lib/cushion_defaults/class_methods.rb, line 510
def make_polite(*syms)
  syms.each do |sym|
    @defaults.not_pushy!(sym)
    _update_cushion_reader(sym, true)
  end
end
make_pushy(*syms) click to toggle source

Declare each sym to be pushy. Pushy defaults will be returned by {#cushion_reader}s regardless of what the instance variables are set to.

This will automatically create {#cushion_reader}‘s for all variables made pushy.

# File lib/cushion_defaults/class_methods.rb, line 499
def make_pushy(*syms)
  syms.each do |sym|
    @defaults.pushy!(sym)
    _update_cushion_reader(sym, true)
  end
end
remove_bang(sym) click to toggle source

Undefines any bang method for sym in this class (if present). Does not affect ancestor classes. @note This method will delete any method of the form @sym!@, not just bang readers. @param sym [#to_sym] instance variable to delete bang method for @deprecated

# File lib/cushion_defaults/class_methods.rb, line 315
def remove_bang(sym)
  sym = "#{sym}!".to_sym
  if self_has_method?(sym)
    undef_method(sym)
    @have_bang_readers.delete(sym)
    CushionDefaults.log("bang reader #{sym} removed from #{self}", :info)
  end
end
remove_cushion_bang(sym) click to toggle source

Undefines any bang method for sym in this class (if present). Does not affect ancestor classes. @note This method will delete any method of the form @sym!@, not just bang readers. @param sym [#to_sym] instance variable to delete bang method for

# File lib/cushion_defaults/class_methods.rb, line 327
def remove_cushion_bang(sym)
  sym = "#{sym}!".to_sym
  if @have_bang_readers.delete?(sym) && self_has_method?(sym)
    undef_method(sym)
    CushionDefaults.log("bang reader #{sym} removed from #{self}", :info)
  end
end
remove_cushion_reader(sym) click to toggle source

Undefines the cushion_reader for sym in this class (if present). Does not affect ancestor classes. @param sym [#to_sym] instance variable to delete cushion_reader for

# File lib/cushion_defaults/class_methods.rb, line 303
def remove_cushion_reader(sym)
  sym = sym.to_sym
  if @have_cushion_readers.delete?(sym) && self_has_method?(sym)
    undef_method(sym)
    CushionDefaults.log("cushion_reader #{sym} removed from #{self}", :info)
  end
end
remove_cushion_writer(sym) click to toggle source

Undefines any writer for sym in this class (if present). Does not affect ancestor classes. @note This method will delete any method of the format ‘sym=’, not just cushion_writers. @param sym [#to_sym] instance variable to delete writer for

# File lib/cushion_defaults/class_methods.rb, line 351
def remove_cushion_writer(sym)
  write_sym = "#{sym}=".to_sym
  if @have_cushion_writers.delete?(write_sym) && self_has_method?(write_sym)
    undef_method(write_sym)
    CushionDefaults.log("cushion_writer #{sym}= removed from #{self}", :info)
  end
end
remove_reader(sym) click to toggle source

Undefines the reader for sym in this class (if present). Does not affect ancestor classes. @note This method will delete any method of the form @sym@, not just cushion_readers. @param sym [#to_sym] instance variable to delete reader for @deprecated

# File lib/cushion_defaults/class_methods.rb, line 292
def remove_reader(sym)
  sym = sym.to_sym
  if self_has_method?(sym)
    undef_method(sym)
    @have_cushion_readers.delete(sym)
    CushionDefaults.log("cushion_reader #{sym} removed from #{self}", :info)
  end
end
remove_writer(sym) click to toggle source

Undefines any writer for sym in this class (if present). Does not affect ancestor classes. @note This method will delete any method of the format ‘sym=’, not just cushion_writers. @param sym [#to_sym] instance variable to delete writer for @deprecated

# File lib/cushion_defaults/class_methods.rb, line 339
def remove_writer(sym)
  write_sym = "#{sym}=".to_sym
  if self_has_method?(write_sym)
    undef_method(write_sym)
    @have_cushion_writers.delete(write_sym)
    CushionDefaults.log("cushion_writer #{sym}= removed from #{self}", :info)
  end
end
set_default(key, val) click to toggle source

Equivalent to @@defaults = val@ @param key [#to_sym] key for new default. @param val [Object] Default value for @key@.

# File lib/cushion_defaults/class_methods.rb, line 32
def set_default(key, val)
  @defaults[key.to_sym] = val
end
thaw_default(*syms) click to toggle source

Thaws a frozen default, allowing it to be set to a new value.

Note that if the value of the @default@ is itself frozen (using {#deep_freeze_default} or @Object#freeze@), thaw_default allows you to set @default@ to a new value but does not and cannot allow you to modify the value to which @default@ points until and unless @default@ is set to a new value.

Does not throw an error when you attempt to thaw a non-frozen default, but it does log at warning level.

@api freeze

# File lib/cushion_defaults/class_methods.rb, line 475
def thaw_default(*syms)
  syms.each do |sym|
    sym = sym.to_sym
    unless defaults.thaw_default! sym
      # It returns nil if the key was not present in the Set
      CushionDefaults.log("Cannot thaw #{sym}: it was already thawed for #{self}", :warn)
    end
    CushionDefaults.log("After thawing, currently frozen defaults are #{defaults.frozen_defaults.to_a.join(', ')}")
  end
end
thaw_defaults() click to toggle source

Calls {#thaw_default} for all defaults.

@api freeze

# File lib/cushion_defaults/class_methods.rb, line 489
def thaw_defaults
  thaw_default *defaults.keys
end

Protected Instance Methods

self_has_method?(sym) click to toggle source

Check whether this class includes the instance method denoted by @sym@ @param sym [#to_sym] method in question

# File lib/cushion_defaults/class_methods.rb, line 573
def self_has_method?(sym)
  instance_methods(false).include?(sym.to_sym)
end
self_or_parent_instance_method?(sym) click to toggle source

Check whether this class or any parent class includes the instance method denoted by @sym@ @param sym [#to_sym] method in question

# File lib/cushion_defaults/class_methods.rb, line 567
def self_or_parent_instance_method?(sym)
  instance_methods(true).include?(sym.to_sym)
end

Private Instance Methods

_cascade_polite_symbol(sym) click to toggle source

Called when a variable either becomes polite or may have become polite.

# File lib/cushion_defaults/class_methods.rb, line 540
def _cascade_polite_symbol(sym)
  # if we get down far enough, it may become pushy again
  return unless @defaults.not_pushy? sym
  _reveal_cushion_reader(sym) if self_has_method? "_#{sym}_hidden".to_sym
  # cascade it down to descendents
  @descendents.each { |desc| desc._cascade_polite_symbol sym if respond_to? :_cascade_polite_symbol }
end
_cascade_pushy_symbol(sym) click to toggle source

Called when a variable either becomes pushy or may have become pushy.

# File lib/cushion_defaults/class_methods.rb, line 518
def _cascade_pushy_symbol(sym)
  # if we get down far enough, it may no longer be pushy (we read a polite)
  # at that point, we can assume accurate state
  return unless @defaults.pushy? sym
  if self_has_method? sym
    if @defaults.pushy_in_parent? sym
      # if it's pushy in a parent now, then hide the reader
      # we hide it rather than delete it because we want to be able to restore a manually-created cushion_reader if
      # this is later made not-polite
      _hide_cushion_reader sym
      @have_cushion_readers.delete sym
      # cascade it down to descendents
      @descendents.each { |desc| desc._cascade_pushy_symbol sym if respond_to? :_cascade_pushy_symbol }
    else
      # if it's pushy in us (has to be, if not pushy in parent) then just update the reader
      _update_cushion_reader sym
      # also, if it's pushy in us, we don't need to look at descendents any more, so no need to call
    end
  end
end
_define_cushion_reader_no_pushy_dynamic(sym) click to toggle source

@api private

# File lib/cushion_defaults/class_methods.rb, line 130
def _define_cushion_reader_no_pushy_dynamic(sym)
  defaults_cache = @defaults
  instance_variable_sym = "@#{sym}".to_sym
  define_method(sym) do
    instance_variable_defined?(instance_variable_sym) ? instance_variable_get(instance_variable_sym) : defaults_cache[sym].call(self, sym)
  end
end
_define_cushion_reader_no_pushy_static(sym) click to toggle source

@api private

# File lib/cushion_defaults/class_methods.rb, line 139
def _define_cushion_reader_no_pushy_static(sym)
  defaults_cache = @defaults
  instance_variable_sym = "@#{sym}".to_sym
  define_method(sym) do
    instance_variable_defined?(instance_variable_sym) ? instance_variable_get(instance_variable_sym) : defaults_cache[sym]
  end
end
_define_cushion_reader_pushy_dynamic(sym) click to toggle source

@api private

# File lib/cushion_defaults/class_methods.rb, line 113
def _define_cushion_reader_pushy_dynamic(sym)
  defaults_cache = @defaults
  define_method(sym) do
    defaults_cache[sym].call(self, sym)
    da_default.respond_to?(:call) ? da_default.call(self, sym) : da_default
  end
end
_define_cushion_reader_pushy_static(sym) click to toggle source

@api private

# File lib/cushion_defaults/class_methods.rb, line 122
def _define_cushion_reader_pushy_static(sym)
  defaults_cache = @defaults
  define_method(sym) do
    defaults_cache[sym]
  end
end
_hide_cushion_reader(sym) click to toggle source
# File lib/cushion_defaults/class_methods.rb, line 359
def _hide_cushion_reader(sym)
  # Called (1) when a cushion reader exists and (2) the reader needs to be hidden
  alias_method "_#{sym}_hidden".to_sym, sym
  remove_method sym
end
_reveal_cushion_reader(sym) click to toggle source
# File lib/cushion_defaults/class_methods.rb, line 365
def _reveal_cushion_reader(sym)
  # Called (1) when a previous cushion reader exists but (2) it was hidden due to a parent's sym becoming pushy
  alias_method sym, "_#{sym}_hidden".to_sym
  remove_method "_#{sym}_hidden".to_sym
  # Things might have changed, so we should update the method
  @have_cushion_readers.add sym
  _update_cushion_reader sym
end
_set_up_cushion_method_sets() click to toggle source
# File lib/cushion_defaults/class_methods.rb, line 78
def _set_up_cushion_method_sets
  @have_cushion_readers = Set.new
  @have_bang_readers = Set.new
  @have_cushion_writers = Set.new
  @descendents = Set.new
end
_update_cushion_reader(sym, force=false) click to toggle source

@param sym [Symbol] the attribute to update the cushion_reader for @param force [boolean] if true, it will always update the method @api private

# File lib/cushion_defaults/class_methods.rb, line 152
def _update_cushion_reader(sym, force=false)
  # This will redefine a method each time the default is changed, but at this point that does not seem like a big
  # enough performance hit to warrant the checks required to avoid it.

  # force: should we force it to continue? (Basically should only be forced by {#cushion_reader})
  # include?: if the reader already exists (i.e., it was previously manually created) then continue
  # update_readers: if we're updating readers automatically, then continue
  return unless force || @have_cushion_readers.include?(sym) || CushionDefaults.conf.update_readers

  # include?: so we only show this warning once
  if !@have_cushion_readers.include?(sym) && self_or_parent_instance_method?(sym)
    CushionDefaults.log("#{self} or a parent class already has what looks like an attr_reader for #{sym}", :warn)
  end

  # Create one of four cushion_reader methods depending on the state and type of the default value
  if @defaults.pushy? sym
    @defaults[sym].respond_to?(:call) ? _define_cushion_reader_pushy_dynamic(sym) : _define_cushion_reader_pushy_static(sym)
  else
    default = @defaults[sym]
    default.respond_to?(:call) ? _define_cushion_reader_no_pushy_dynamic(sym) : _define_cushion_reader_no_pushy_static(sym)
  end
  @have_cushion_readers.add sym
  CushionDefaults.log("cushion_reader #{sym} updated for #{self}")
end