Class: AttrChain
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. # 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
-
#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.
-
#initialize(clazz, variable_name, attr_configs) ⇒ AttrChain
constructor
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.
-
#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.
-
#set_parameters(variable_name, params) ⇒ Object
Parses long syntax values and sets configuration for this field.
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 |