class AttrChain
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("test@email.com").name("test") foo.email => "test@email.com" 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 usesInstanceVariableAccessor
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 andattr_chain
count below 10.
@see InstanceVariableAccessor
@see Object.attr_chain
@see Module#attr_chain
Constants
- HashAccess
- InstanceVariableAccess
Public Class Methods
Parses parameters with parse_short_syntax
and set_parameters
and configures class methods
-
each
attr_chain
definition uses one instance ofAttrChain
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
@see Object.attr_chain
@see Module#attr_chain
# 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
Public Instance Methods
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
# 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
Converts short syntax entries in attr_configs to long syntax
-
warns about not supported values and already defined values
# 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
Parses long syntax values and sets configuration for this field
# 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