Class: AttrChain

Inherits:
Object show all
Defined in:
lib/util/attr_chain.rb

Overview

Attaches configurable behaviour to accessor methods

class Foo
   attr_chain :name, :require
   attr_chain :email, -> {""}
   attr_chain :birth_day, :immutable, :valid => lambda { |i| (1870..Time.now.year+1).include?(i) }, :require => true
   attr_chain :children, :convert => lambda {|s| s.to_i}
end

Sets up public methods variable_name and variable_name= which both can be used to access the fields. Giving any parameters for the method makes it a “set” operation and giving no parameters makes it a “get” operation. “Set” stores the value and returns self, so set calls can be chained. “Get” returns the stored value.

foo.email("[email protected]").name("test")
foo.email => "[email protected]"
foo.name => "test"

Parameters can be given in short and long format. Short format works by identifying parameter types, long format works by given the name and value as hash parameters:

  • :require=>"You need to define xxx first", :require=>true, short: :require - an exception is thrown if target field is not defined

  • :default=> -> {true}, short: -> {Array.new} - if target field has not been defined, executes proc and stores value. proc is executed using object.instance_exec: object’s fields & methds are available

  • :immutable=>true, short: :immutable - an exception is thrown if target field is defined a second time

  • :valid=>[1,2,3,"a", lambda {|s| s.include?("b")}], :valid => lambda {|s| s.include?("b")}, short: [1,2,3,"a"] - List of valid values. If any matches, sets value. If none matches, raises exception. Long form wraps single arguments to a list.

  • :convert=> ->(s) { s+1 } - Converts input value using the defined proc

  • :accessor=>InstanceVariableAccessor.new - Makes it possible to set values in other source, for example a hash. By default uses InstanceVariableAccessor

Advantages for using attr_chain

  • attr_chain has a compact syntax for many important programming concepts -> less manually written boilerplate code is needed

  • :default makes it easy to isolate functionality to a default value while still making it easy to override the default behaviour

  • :default adds easy lazy evalution and memoization to the attribute, default value is evaluated only if needed

  • Testing becomes easier when objects have more exposed fields

  • :require converts tricky nil exceptions in to useful errors. Instead of the “undefined method ‘bar’ for nil:NilClass” you get a good error message that states which field was not defined

    foo.name.bar # if name has not been defined, raises "'name' has not been set" exception
    
  • :immutable, :valid and :convert make complex validations and converts easy

Warnings about attr_chain

  • Performance has not been measured and attr_chain is probably not efficient. If there are tight inner loops, it’s better to cache the value and store it afterwards

  • There has not been tests for memory leaks. It’s plain ruby so GC should take care of everything

  • Excessive attr_chain usage makes classes a mess. Try to keep your classes short and attr_chain count below 10.

Defined Under Namespace

Classes: HashAccessor, InstanceVariableAccessor

Constant Summary collapse

InstanceVariableAccess =
InstanceVariableAccessor.new
HashAccess =
HashAccessor.new

Instance Method Summary collapse

Constructor Details

#initialize(clazz, variable_name, attr_configs) ⇒ AttrChain

Parses parameters with parse_short_syntax and set_parameters and configures class methods

  • each attr_chain definition uses one instance of AttrChain which holds the configuration for the definition

  • Object::define_method is used to add two methods to target class and when called both of these methods call attr_chain with their parameters



65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/util/attr_chain.rb', line 65

def initialize(clazz, variable_name, attr_configs)
  @variable_name = variable_name
  @accessor = InstanceVariableAccess
  set_parameters(variable_name, parse_short_syntax(variable_name, attr_configs))
  me = self
  attr_call = lambda { |*args| me.attr_chain(self, args) }
  [variable_name, "#{variable_name}="].each do |method_name|
    if clazz.method_defined?(method_name)
      clazz.send(:undef_method, method_name)
    end
    clazz.send(:define_method, method_name, attr_call)
  end
end

Instance Method Details

#attr_chain(object, args) ⇒ Object

Handles incoming methods for “get” and “set”

  • called by methods defined to class

  • configuration is stored as instance variables, the class knows which variable is being handled

  • method call parameters come as list of parameters



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/util/attr_chain.rb', line 152

def attr_chain(object, args)
  if args.empty?
    if !@accessor.defined?(object, @variable_name)
      if defined? @default
        @accessor.set(object, @variable_name, object.instance_exec(&@default))
      elsif defined? @require
        if @require.kind_of?(String)
          raise "'#{@variable_name}' has not been set: #{@require}"
        else
          raise "'#{@variable_name}' has not been set"
        end
      end
    end
    @accessor.get(object, @variable_name)
  else
    if defined?(@immutable) && @accessor.defined?(object, @variable_name)
      raise "'#{@variable_name}' has been set once already"
    end
    value_to_set = if args.size == 1
                     args.first
                   else
                     args
                   end
    if defined? @convert
      value_to_set = object.instance_exec(value_to_set, &@convert)
    end
    if defined?(@valid_items) || defined?(@valid_procs)
      is_valid = false
      if defined?(@valid_items) && @valid_items.include?(value_to_set)
        is_valid = true
      end
      if is_valid == false && defined?(@valid_procs)
        @valid_procs.each do |valid_proc|
          if is_valid=object.instance_exec(value_to_set, &valid_proc)
            break
          end
        end
      end
      if is_valid == false
        raise "invalid value for '#{@variable_name}'"
      end
    end
    @accessor.set(object, @variable_name, value_to_set)
    object
  end
end

#parse_short_syntax(variable_name, attr_configs) ⇒ Object

Converts short syntax entries in attr_configs to long syntax

  • warns about not supported values and already defined values



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/util/attr_chain.rb', line 81

def parse_short_syntax(variable_name, attr_configs)
  params = {}
  attr_configs.each do |attr_config|
    key_values = if [:require, :immutable].include?(attr_config)
                   [[attr_config, true]]
                 elsif attr_config.kind_of?(Proc)
                   [[:default, attr_config]]
                 elsif attr_config.kind_of?(Array)
                   [[:valid, attr_config]]
                 elsif attr_config.kind_of?(Hash)
                   all = []
                   attr_config.each_pair do |pair|
                     all << pair
                   end
                   all
                 else
                   raise "attr_chain :#{variable_name} unsupported parameter: '#{attr_config.inspect}'"
                 end
    key_values.each do |key, value|
      if params.include?(key)
        raise "attr_chain :#{variable_name}, :#{key} was already defined to '#{params[key]}' (new value: '#{value}')"
      end
      params[key]=value
    end
  end
  params
end

#set_parameters(variable_name, params) ⇒ Object

Parses long syntax values and sets configuration for this field



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/util/attr_chain.rb', line 110

def set_parameters(variable_name, params)
  params.each_pair do |key, value|
    case key
      when :require
        @require = value
      when :default
        if !value.kind_of?(Proc)
          raise "attr_chain :#{variable_name}, :default needs to be a Proc, not '#{value.inspect}'"
        end
        @default = value
      when :immutable
        @immutable = value
      when :valid
        if !value.kind_of?(Array)
          value = [value]
        end
        value.each do |valid|
          if valid.kind_of?(Proc)
            @valid_procs ||= []
            @valid_procs << valid
          else
            @valid_items ||= {}
            @valid_items[valid]=valid
          end
        end
      when :convert
        if !value.kind_of?(Proc)
          raise "attr_chain :#{variable_name}, :convert needs to be a Proc, not '#{value.inspect}'"
        end
        @convert = value
      when :accessor
        @accessor = value
      else
        raise "attr_chain :#{variable_name} unsupported parameter: '#{key.inspect}'"
    end
  end
end