Permalink
Browse files

Refactoring of DefinedHash and further specs

* Now handles DefinedHash properties, and arrays of such
* cleaner implementation, I think
  • Loading branch information...
1 parent 62ae592 commit d6c1b59ccca9dfa9a3018c657ad466954f3ad533 @namelessjon committed Jul 4, 2010
Showing with 153 additions and 36 deletions.
  1. +76 −36 lib/hash.rb
  2. +77 −0 test/suite/lib/hash.rb
View
112 lib/hash.rb
@@ -10,63 +10,103 @@
class DefinedHash < Hash
def initialize(attributes={})
- self.class.add_properties(self.class.properties, self, attributes)
+ attributes.each do |name, value|
+ self[name] = value
+ end
end
- def self.property(name, type, opts={})
- validator = opts.fetch(:optional, false) ? proc { |v| v.nil? ? true : type } : type
- (@properties ||= {})[name] = type
- (@validations ||= {})[name] = validator
+ def []=(name, value)
+ if (property_name = self.class.property_names.detect { |pn| pn == name.to_s })
+ property_name = property_name.to_sym # we know it's a defined name, so this isn't a leak.
+ property = self.class.properties[property_name]
+ case property
+ when Array
+ value = self.class.typecast_value_to_array(property, value)
+ if has_key?(property_name)
+ self[property_name].concat( value )
+ else
+ super(property_name, value)
+ end
+ when Class
+ super(property_name, self.class.typecast_value_to_class(property, value))
+ else
+ super(property_name, value)
+ end
+ end
+ # ignore properties without names
end
- def self.properties
- (@properties || {})
+ def self.typecast_value_to_class(klass, value)
+ if klass < DefinedHash # if we have a defined hash, make it new!
+ value = klass.new(value) unless klass === value
+ end
+ value
end
- def self.validations
- (@validations || {})
- end
- def self.add_properties(properties, hash, attributes)
- properties.each do |name, type|
- add_property(hash, attributes, name, type)
+
+ def self.typecast_value_to_array(property, value)
+ value = (Array === value) ? value : [value]
+ case property.first
+ when Hash
+ return value.map { |v| self.typecast_value_to_hash(property.first, v) }
+ when Class
+ return value.map { |v| self.typecast_value_to_class(property.first, v) }
end
+ value
end
- def self.add_property(hash, attributes, name, type)
- case type
- when Array
- value = attributes[name] if attributes.has_key?(name)
- value = attributes[name.to_s] if attributes.has_key?(name.to_s)
- if value
- value = (Array === value) ? value : [value]
- case type.first
- when Hash
- array = []
- value.each do |v|
- tmp = {}
- add_properties(type.first, tmp, v)
- array << tmp unless tmp.empty?
- end
- hash[name] = array unless array.empty?
- else
- hash[name] = value
- end
+ def self.typecast_value_to_hash(property, values)
+ hash = {}
+ keys = property.keys.map { |k| k.to_s }
+ values.each do |name, value|
+ name = name.to_s
+ if keys.include?(name)
+ hash[name.to_sym] = value
end
- else
- hash[name] = attributes[name] if attributes.has_key?(name)
- hash[name] = attributes[name.to_s] if attributes.has_key?(name.to_s)
end
+ hash
+ end
+
+ def self.property(name, type, opts={})
+ # use a DefinedHash's inbuilt validations if we have a defined hash
+ klass_validator = class_validator_for(type)
+ klass_validator = (Array === klass_validator and
+ Class === klass_validator.first and
+ klass_validator.first < DefinedHash) ? [class_validator_for(klass_validator.first)] : klass_validator
+
+ # optional validations done via proc
+ validator = opts.fetch(:optional, false) ? proc { |v| v.nil? ? true : klass_validator } : klass_validator
+
+ (@properties ||= {})[name] = type
+ (@validations ||= {})[name] = validator
+ end
+
+ def self.class_validator_for(type)
+ (Class === type and type < DefinedHash) ? proc { |v| v.valid? } : type
+ end
+
+ def self.property_names
+ properties.keys.map { |p| p.to_s }
end
+ def self.properties
+ (@properties || {})
+ end
+
+ def self.validations
+ (@validations || {})
+ end
def valid?
Hashidator.validate(self.class.validations, self)
end
def merge!(hash)
- self.class.add_properties(self.class.properties, self, hash)
+ hash.each do |key, value|
+ self[key] = value
+ end
end
def to_hash
View
77 test/suite/lib/hash.rb
@@ -51,6 +51,48 @@ class ::Person < DefinedHash
end
end
+ suite "DefinedHash properties", :depends_on => :new do
+ setup do
+ class ::Address < DefinedHash
+ property :name, String
+ property :postcode, String
+ end
+
+ class ::Email < DefinedHash
+ property :name, String
+ property :email, String
+ end
+
+ class ::Person
+ property :address, ::Address
+ property :emails, [::Email]
+ end
+ end
+
+ teardown do
+ Object.send(:remove_const, :Address)
+ end
+
+ suite do
+ setup :person, [
+ {:name => 'name', :address => { :name => 'home', :postcode => 'code' }},
+ {:name => 'name', :address => { 'name' => 'home', 'postcode' => 'code' }}
+# {:name => 'name', :address => Address.new('name' => 'home', 'postcode' => 'code' )}
+ ] do |person|
+ @person = Person.new(person)
+ end
+
+
+ assert ":person has address name set correctly" do
+ @person[:address][:name] == 'home'
+ end
+
+ assert ":person has address postcode set correctly" do
+ @person[:address][:postcode] == 'code'
+ end
+ end
+ end
+
suite "Array properties", :depends_on => :new do
setup do
class ::Person
@@ -77,14 +119,28 @@ class ::Person
@person = Person.new(:emails => [{ 'name' => 'gmail', :foo => 'bar'}])
equal_unordered([{:name => 'gmail'}], @person[:emails])
end
+
+
end
suite "Testing validity", :depends_on => :new do
setup do
+ class ::Email < DefinedHash
+ property :name, String, :optional => true
+ property :email, String
+ end
+
+ class ::Number < DefinedHash
+ property :name, String, :optional => true
+ property :number, String
+ end
+
class ::Person
property :nick, String, :optional => true
+ property :email, Email, :optional => true
+ property :numbers, [Number], :optional => true
end
end
@@ -94,6 +150,8 @@ class ::Person
{:page => 'page'},
{ :name => 'name', :page => 1 },
{ :name => 1, :page => 'page' },
+ {:name => 'name', :page => 'page', :email => { :name => 'home'}},
+ {:name => 'name', :page => 'page', :email => { :email => 'a@example.com'}, :numbers => [{ :number => "555-1234" }, { }]},
{:name => 'name', :page => 'page', :nick => 2 }
] do |invalid_person|
@person = Person.new(invalid_person)
@@ -107,6 +165,9 @@ class ::Person
suite "valid" do
setup :valid_person, [
{:name => 'name', :page => 'page'},
+ {:name => 'name', :page => 'page', :email => { :name => 'home', :email => 'a@example.com'}},
+ {:name => 'name', :page => 'page', :email => { :email => 'a@example.com'}},
+ {:name => 'name', :page => 'page', :email => { :email => 'a@example.com'}, :numbers => [{ :number => "555-1234" }]},
{:name => 'name', :page => 'page', :nick => 'namz' }
] do |valid_person|
@person = Person.new(valid_person)
@@ -117,4 +178,20 @@ class ::Person
end
end
end
+
+ suite "merge!", :depends_on => :new do
+ setup do
+ @person = Person.new(:name => 'name', :page => 'p')
+ end
+
+ assert "properties are merged correctly from a hash" do
+ @person.merge!(:page => 'page', :foo => 'bar')
+ @person == Person.new(:name => 'name', :page => 'page')
+ end
+
+ assert "properties are merged correctly from a person" do
+ @person.merge!(Person.new(:page => 'page', :foo => 'bar'))
+ @person == Person.new(:name => 'name', :page => 'page')
+ end
+ end
end

0 comments on commit d6c1b59

Please sign in to comment.