Class: Valuable

Inherits:
Object
  • Object
show all
Defined in:
lib/valuable.rb

Overview

Valuable is the class from which all classes (who are so inclined) should inherit.

Example:

class Bus < Valuable

  has_value :number, :klass => :integer
  has_value :color, :default => 'yellow'
  has_collection :riders, :alias => 'Passengers'

end

>> Bus.attributes
=> [:number, :color, :riders]
>> bus = Bus.new(:number => '3', :Passengers => ['GOF', 'Fowler', 'Mort']
>> bus.attributes
=> {:number => 3, :riders => ['GOF', 'Fowler', 'Mort'], :color => 'yellow'}

Defined Under Namespace

Modules: Utils

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(atts = nil) ⇒ Valuable

accepts an optional hash that will be used to populate the predefined attributes for this class.

Note: You are free to overwrite the constructor, but you should call initialize_attributes OR make sure at least one value is stored.



54
55
56
57
# File 'lib/valuable.rb', line 54

def initialize(atts = nil)
  initialize_attributes
  self.update_attributes(atts || {})
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args) ⇒ Object



89
90
91
92
93
94
95
# File 'lib/valuable.rb', line 89

def method_missing(method_name, *args)
  if method_name.to_s =~ /(\w+)=/
    raise( ArgumentError, "#{self.class.to_s} does not have an attribute or alias '#{$1}'", caller) unless self.permissive?
  else
    super 
  end
end

Class Method Details

._attributesObject



114
115
116
# File 'lib/valuable.rb', line 114

def _attributes
  @_attributes ||= {}
end

.acts_as_permissiveObject

Instructs the class NOT to complain if any attributes are set that haven’t been declared.

class Sphere < Valuable

has_value :material

end

>> Sphere.new(:radius => 3, :material => ‘water’) EXCEPTION! OH NOS!

class Box < Valuable

acts_as_permissive

has_value :material

end

>> box = Box.new(:material => ‘wood’, :size => ‘36 x 40’) >> box.attributes

> => ‘wood’



353
354
355
# File 'lib/valuable.rb', line 353

def acts_as_permissive
  self.permissive_constructor=true
end

.attributesObject

Returns an array of the attributes available on this object.



110
111
112
# File 'lib/valuable.rb', line 110

def attributes
  _attributes.keys
end

.create_accessor_for(name, extensions) ⇒ Object

creates an accessor method named after the attribute… can be used as a chained setter, as in:

whitehouse.windows(5).doors(4).oval_rooms(1)

If NOT used as a setter, returns the value, extended by the modules listed in the second parameter.



217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/valuable.rb', line 217

def create_accessor_for(name, extensions)
  define_method name do |*args|
    if args.length == 0
      attributes[name].tap do |out|
        extensions.each do |extension|
          out.extend( extension )
        end
      end
    else
      send("#{name}=", *args)
      self
    end 
  end
end

.create_negative_question_for(name, negative) ⇒ Object

In some situations, the opposite of a value may be just as interesting.

class Coder < Valuable
  has_value :agilist, :klass => Boolean, :negative => :waterfaller
end

monkey = Coder.new(:agilist => false)
>> monkey.waterfaller?
=> true


257
258
259
260
261
# File 'lib/valuable.rb', line 257

def create_negative_question_for(name, negative)
  define_method "#{negative}?" do
    !attributes[name]
  end
end

.create_question_for(name) ⇒ Object

In addition to the normal getter and setter, boolean attributes get a method appended with a ?.

class Player < Valuable
  has_value :free_agent, :klass => Boolean
end

juan = Player.new(:free_agent => true)
>> juan.free_agent?
=> true


242
243
244
245
246
# File 'lib/valuable.rb', line 242

def create_question_for(name)
  define_method "#{name}?" do
    attributes[name]
  end
end

.create_setter_for(attribute, options) ⇒ Object

Creates the method that sets the value of an attribute. The setter calls write_attribute, which handles typicification. It is called by the constructor (rather than using write attribute, which would render any custom setters ineffective.)

Setting values via the attributes hash avoids typification, ie: >> player.phone = “8778675309” >> player.phone

> “(877) 867-5309”

>> player.attributes = “8778675309” >> player.phone

> “8778675309”



192
193
194
195
196
197
198
199
200
# File 'lib/valuable.rb', line 192

def create_setter_for(attribute, options)
  setter_method = "#{attribute}="

  define_method setter_method do |value|
    if options[:allow_blank] || value != ""
      write_attribute(attribute, value)
    end
  end
end

.defaultsObject

Returns a name/value set of the values that will be used on instanciation unless new values are provided.

>> Bus.defaults
=> {:color => 'yellow'}


123
124
125
126
127
# File 'lib/valuable.rb', line 123

def defaults
  out = {}
  _attributes.each{|n, atts| out[n] = atts[:default] unless atts[:default].nil?}
  out
end

.has_collection(name, options = {}) ⇒ Object

this is a more intuitive way of marking an attribute as holding a collection.

class Bus < Valuable
  has_value :riders, :default => [] # meh...
  has_collection :riders # better!
end

>> bus = Bus.new
>> bus.riders << 'jack'
>> bus.riders
=> ['jack']

class Person
  has_collection :phone_numbers, :klass => PhoneNumber
end

>> jenny = Person.new(:phone_numbers => ['8008675309'] )
>> jenny.phone_numbers.first.class
=> PhoneNumber


283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/valuable.rb', line 283

def has_collection(name, options = {})
  Utils.check_options_validity( self.class.name, name, options)
  name = name.to_sym
  options[:item_klass] = options[:klass] if options[:klass]
  options[:klass] = :collection
  options[:default] ||= []
  options[:extend] = [options[:extend]].flatten.compact

  _attributes[name] = options 
  
  create_accessor_for(name, options[:extend])
  create_setter_for(name, allow_blank: false)

  sudo_alias options[:alias], name if options[:alias]
  sudo_alias "#{options[:alias]}=", "#{name}=" if options[:alias]
end

.has_value(name, options = {}) ⇒ Object

Decorator method that lets you specify the attributes for your model. It accepts an attribute name (a symbol) and an options hash. Valid options are :default, :klass and (when :klass is Boolean) :negative.

:default - for the given attribute, use this value if no other
is provided.

:klass - light weight type casting. Use :integer, :string or
:boolean. Alternately, supply a class. 

:alias - creates an alias for getter and setter with the new name.

When a :klassified attribute is set to some new value, if the value is not nil and is not already of that class, the value will be cast to the specified klass. In the case of :integer, it wil be done via .to_i. In the case of a random other class, it will be done via Class.new(value). If the value is nil, it will not be cast.

A good example: PhoneNumber < String is useful if you want numbers to come out the other end properly formatted, when your input may come in as an integer, or string without formatting, or string with bad formatting.

IMPORTANT EXCEPTION

Due to the way Rails handles checkboxes, ‘0’ resolves to FALSE, though it would normally resolve to TRUE.



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/valuable.rb', line 157

def has_value(name, options={})
  Valuable::Utils.check_options_validity(self.class.name, name, options)

  options[:extend] = [options[:extend]].flatten.compact
  options[:allow_blank] = options.has_key?(:allow_blank) ? options[:allow_blank] : true

  name = name.to_sym
  _attributes[name] = options 
 
  create_accessor_for(name, options[:extend])

  create_question_for(name) if options[:klass] == :boolean
  create_negative_question_for(name, options[:negative]) if options[:klass] == :boolean && options[:negative]
  
  create_setter_for(name, allow_blank: options[:allow_blank] )

  sudo_alias options[:alias], name if options[:alias]
  sudo_alias "#{options[:alias]}=", "#{name}=" if options[:alias]
end

.permissive_constructor=(value) ⇒ Object



357
358
359
# File 'lib/valuable.rb', line 357

def permissive_constructor=(value)
  @_permissive_constructor = value
end

.permissive_constructor?Boolean

Returns:

  • (Boolean)


361
362
363
# File 'lib/valuable.rb', line 361

def permissive_constructor?
  !!(@_permissive_constructor ||= false)
end

.register_formatter(name, &block) ⇒ Object

Register custom formatters. Not happy with the default behavior? Custom formatters override all pre-defined formatters. However, remember that formatters are defined globally, rather than per-class.

Valuable.register_formatter(:orientation) do |value|

case value
case Numeric
  value
when 'N', 'North'
  0 
when 'E', 'East'
  90 
when 'S', 'South'
  180 
when 'W', 'West'
  270 
else
  nil 
end

end

class MarsRover < Valuable

has_value :orientation, :klass => :orientation

end

>> curiosity = MarsRover.new(:orientation => ‘S’) >> curiosity.orientation

> 180



329
330
331
# File 'lib/valuable.rb', line 329

def register_formatter(name, &block)
  Valuable::Utils.formatters[name] = block
end

.sudo_alias(alias_name, method_name) ⇒ Object



202
203
204
205
206
# File 'lib/valuable.rb', line 202

def sudo_alias( alias_name, method_name )
  define_method alias_name do |*atts|
    send(method_name, *atts)
  end
end

Instance Method Details

#attributesObject

Returns a Hash representing all known values. Values are set four ways:

(1) Default values are set on instanciation, ie Person.new
(2) they were passed to the constructor
       Bus.new(:color => 'green')
(3) they were set via their namesake setter or alias setter
       bus.color = 'green'
       bus.Passengers = ['bill', 'steve']
(4) the write_attributes(key, value) method

Values that have not been set and have no default not appear in this collection. Their namesake attribute methods will respond with nil. Always use symbols to access these values, ie:

Person.attributes[:color]

not

Person.attributes['color']

basic usage:

>> bus = Bus.new(:number => 16) # color has default value 'yellow'
>> bus.attributes
=> {:color => 'yellow', :number => 16}


43
44
45
# File 'lib/valuable.rb', line 43

def attributes
  @attributes ||= Valuable::Utils.initial_copy_of_attributes(self.class.defaults) 
end

#initialize_attributesObject

Returns a Hash representing all known values. Values are set four ways:

(1) Default values are set on instanciation, ie Person.new
(2) they were passed to the constructor
       Bus.new(:color => 'green')
(3) they were set via their namesake setter or alias setter
       bus.color = 'green'
       bus.Passengers = ['bill', 'steve']
(4) the write_attributes(key, value) method

Values that have not been set and have no default not appear in this collection. Their namesake attribute methods will respond with nil. Always use symbols to access these values, ie:

Person.attributes[:color]

not

Person.attributes['color']

basic usage:

>> bus = Bus.new(:number => 16) # color has default value 'yellow'
>> bus.attributes
=> {:color => 'yellow', :number => 16}


46
47
48
# File 'lib/valuable.rb', line 46

def attributes
  @attributes ||= Valuable::Utils.initial_copy_of_attributes(self.class.defaults) 
end

#permissive?Boolean

Returns:

  • (Boolean)


85
86
87
# File 'lib/valuable.rb', line 85

def permissive?
  self.class.permissive_constructor?
end

#update_attributes(atts) ⇒ Object

mass assign attributes. This method will not clear any existing attributes.

class Shoe

has_value :size
has_value :owner
has_value :color, :default => 'red'

def big_feet?
  size && size > 15
end

end

>> shoe = Shoe.new >> shoe.update_attributes(:size => 16, :owner => ‘MJ’) >> shoe.attributes

> => 16, :owner => ‘MJ’, :color => ‘red’

can be method-chained

>> Shoe.new.update_attributes(:size => 16).big_feet?

> true



80
81
82
83
# File 'lib/valuable.rb', line 80

def update_attributes(atts)
  atts.each{|name, value| __send__("#{name}=", value )}
  self
end

#write_attribute(name, value) ⇒ Object



97
98
99
100
101
102
103
104
105
# File 'lib/valuable.rb', line 97

def write_attribute(name, value)
  attribute = Valuable::Utils.find_attribute_for( name, self.class._attributes )

  if attribute
    self.attributes[attribute] = Valuable::Utils.format(attribute, value, self.class._attributes) 
  else
    raise( ArgumentError, "#{self.class.to_s} does not have an attribute or alias '#{name}'", caller) unless self.permissive?
  end
end