defined_hash 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ .baretest_id_*
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Jonathan Stott
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,42 @@
1
+ = Defined Hash
2
+
3
+ A hash with defined keys
4
+
5
+ This is somewhat similar to the 'Dash' provided by Hashie, but it has
6
+ hashidator validations baked in, and in addition, supports the idea of
7
+ optional columns. It also allows for sensible merging of such values.
8
+ Property definition is based on the hashidator validation syntax.
9
+
10
+ class Person < DefinedHash
11
+ property :name, String # name is a string
12
+ property :emails, [String] # Array of strings for email addresses
13
+ property :addresses, [Address] # Supports nesting of defined hashes
14
+ end
15
+
16
+ It was written with mongodb in mind, as all the existing mappers were massively
17
+ complex for what I wanted.
18
+
19
+ == Who should use DefinedHash?
20
+
21
+ * People who want a simple hash schema with validations
22
+ * People who want something lightweight and fast
23
+
24
+ == Who shouldn't use DefinedHash?
25
+
26
+ * People who want error messages
27
+ * People with a very complex schema
28
+ * People who want an ORM/ODM.
29
+
30
+ == Note on Patches/Pull Requests
31
+
32
+ * Fork the project.
33
+ * Make your feature addition or bug fix.
34
+ * Add tests for it. This is important so I don't break it in a
35
+ future version unintentionally.
36
+ * Commit, do not mess with rakefile, version, or history.
37
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
38
+ * Send me a pull request. Bonus points for topic branches.
39
+
40
+ == Copyright
41
+
42
+ Copyright (c) 2010 Jonathan Stott. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gem|
4
+ gem.name = "defined_hash"
5
+ gem.summary = %Q{Defined hashlike objects, with validations}
6
+ gem.description = %Q{Defined hashlike objects, with validations. For simple schema definitions and checking they're followed}
7
+ gem.email = "jonathan.stott@gmail.com"
8
+ gem.homepage = "http://github.com/namelessjon/defined_hash"
9
+ gem.authors = ["Jonathan Stott"]
10
+ gem.add_dependency "hashidator"
11
+ gem.add_development_dependency "baretest", ">= 0"
12
+ gem.add_development_dependency "yard", ">= 0"
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
18
+ end
19
+
20
+ begin
21
+ require 'yard'
22
+ YARD::Rake::YardocTask.new
23
+ rescue LoadError
24
+ task :yardoc do
25
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
26
+ end
27
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,168 @@
1
+ require 'hashidator'
2
+
3
+ # A hash with defined keys
4
+ #
5
+ # This is somewhat similar to the 'Dash' provided by Hashie, but it has
6
+ # hashidator validations baked in, and in addition, supports the idea of
7
+ # optional columns. It also allows for sensible merging of such values.
8
+ # Property definition is based on the hashidator validation syntax.
9
+ #
10
+ # @example
11
+ # class Person < DefinedHash
12
+ # property :name, String # name is a string
13
+ # property :emails, [String] # Array of strings for email addresses
14
+ # property :addresses, [Address] # Supports nesting of defined hashes
15
+ # end
16
+ #
17
+ class DefinedHash < Hash
18
+
19
+ def initialize(attributes={})
20
+ attributes.each do |name, value|
21
+ self[name] = value
22
+ end
23
+ end
24
+
25
+
26
+ # Assign a value to the hash
27
+ #
28
+ # Assigns a value to the defined hash. This does a certain amount of
29
+ # typecasting, for example converting properties into arrays, or DefinedHashes
30
+ #
31
+ # @param name [Symbol,String] Property name to assign
32
+ # @param value [Object] Value to assign
33
+ #
34
+ # @return [Object] The value just assigned.
35
+ #
36
+ # @api public
37
+ def []=(name, value)
38
+ if (property_name = self.class.property_names.detect { |pn| pn == name.to_s })
39
+ property_name = property_name.to_sym # we know it's a defined name, so this isn't a leak.
40
+ property = self.class.properties[property_name]
41
+ case property
42
+ when Array
43
+ value = self.class.typecast_value_to_array(property, value)
44
+ if has_key?(property_name)
45
+ self[property_name].concat( value )
46
+ else
47
+ super(property_name, value)
48
+ end
49
+ when Class
50
+ super(property_name, self.class.typecast_value_to_class(property, value))
51
+ else
52
+ super(property_name, value)
53
+ end
54
+ end
55
+ # ignore properties without names
56
+ value
57
+ end
58
+
59
+ # Define a property for the hash
60
+ #
61
+ # This is used to define the hash schema. Property definitions look somewhat
62
+ # similar to DataMapper ones, but the types actually define hashidator
63
+ # validation classes, as well as defining the schema.
64
+ #
65
+ # @param name [Symbol] The name of the property
66
+ # @param type [Object] The type of the property
67
+ #
68
+ # @option opts [Boolean] :optional (false) If true, this property won't be validated if it isn't there.
69
+ #
70
+ # @return nil
71
+ #
72
+ # @api public
73
+ def self.property(name, type, opts={})
74
+ # use a DefinedHash's inbuilt validations if we have a defined hash
75
+ klass_validator = class_validator_for(type)
76
+ klass_validator = (Array === klass_validator and
77
+ Class === klass_validator.first and
78
+ klass_validator.first < DefinedHash) ? [class_validator_for(klass_validator.first)] : klass_validator
79
+
80
+ # optional validations done via proc
81
+ validator = opts.fetch(:optional, false) ? proc { |v| v.nil? ? true : klass_validator } : klass_validator
82
+
83
+ (@properties ||= {})[name] = type
84
+ (@validations ||= {})[name] = validator
85
+ nil
86
+ end
87
+
88
+ def self.property_names
89
+ properties.keys.map { |p| p.to_s }
90
+ end
91
+
92
+ def self.properties
93
+ (@properties || {})
94
+ end
95
+
96
+ def self.validations
97
+ (@validations || {})
98
+ end
99
+
100
+ # validates the hash with hashidator
101
+ #
102
+ # @return [Boolean] true/false depending on validation
103
+ #
104
+ # @api public
105
+ def valid?
106
+ Hashidator.validate(self.class.validations, self)
107
+ end
108
+
109
+ # Merges in another hash, destroying original values
110
+ #
111
+ # @param hash [Hash] The hash to merge
112
+ #
113
+ # @return [self] The new hash
114
+ def merge!(hash)
115
+ hash.each do |key, value|
116
+ self[key] = value
117
+ end
118
+ self
119
+ end
120
+
121
+ def to_hash
122
+ out = {}
123
+ keys.each do |k|
124
+ out[k] = self[k]
125
+ end
126
+ out
127
+ end
128
+
129
+
130
+ private
131
+ def self.class_validator_for(type)
132
+ (Class === type and type < DefinedHash) ? proc { |v| v.valid? } : type
133
+ end
134
+
135
+
136
+ def self.typecast_value_to_class(klass, value)
137
+ if klass < DefinedHash # if we have a defined hash, make it new!
138
+ value = klass.new(value) unless klass === value
139
+ end
140
+ value
141
+ end
142
+
143
+
144
+
145
+ def self.typecast_value_to_array(property, value)
146
+ value = (Array === value) ? value : [value]
147
+ case property.first
148
+ when Hash
149
+ return value.map { |v| self.typecast_value_to_hash(property.first, v) }
150
+ when Class
151
+ return value.map { |v| self.typecast_value_to_class(property.first, v) }
152
+ end
153
+ value
154
+ end
155
+
156
+ def self.typecast_value_to_hash(property, values)
157
+ hash = {}
158
+ keys = property.keys.map { |k| k.to_s }
159
+ values.each do |name, value|
160
+ name = name.to_s
161
+ if keys.include?(name)
162
+ hash[name.to_sym] = value
163
+ end
164
+ end
165
+ hash
166
+ end
167
+
168
+ end
data/test/setup.rb ADDED
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
2
+ require 'baretest'
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/ruby
2
+ # Jonathan D. Stott <jonathan.stott@gmail.com>
3
+ require 'defined_hash'
4
+ BareTest.suite "DefinedHash" do
5
+ setup do
6
+ class ::Person < DefinedHash
7
+ property :name, String
8
+ property :page, String
9
+ end
10
+ end
11
+
12
+ teardown do
13
+ Object.send(:remove_const, :Person)
14
+ end
15
+
16
+ suite ".properties" do
17
+ assert "properties list is correct" do
18
+ equal(Person.properties, {:name => String, :page => String})
19
+ end
20
+ end
21
+
22
+ suite ".new", :provides => :new do
23
+ assert "Basic .new works" do
24
+ Person.new
25
+ end
26
+
27
+ assert ".new with properties works" do
28
+ Person.new(:name => 'foo')
29
+ end
30
+
31
+ assert ".new with invalid properties works" do
32
+ Person.new(:name => 'foo', :num => 1)
33
+ end
34
+ end
35
+
36
+ suite "A DefinedHash", :depends_on => :new do
37
+ setup do
38
+ @person = Person.new(:name => 'Bob', 'page' => 'bob', :num => 1)
39
+ end
40
+
41
+ assert "has valid properties set" do
42
+ @person[:name] == 'Bob'
43
+ end
44
+
45
+ assert "has strings set" do
46
+ @person[:page] == 'bob'
47
+ end
48
+
49
+ assert "Doesn't have non-valid keys set" do
50
+ @person.has_key?(:num) == false
51
+ end
52
+ end
53
+
54
+ suite "DefinedHash properties", :depends_on => :new do
55
+ setup do
56
+ class ::Address < DefinedHash
57
+ property :name, String
58
+ property :postcode, String
59
+ end
60
+
61
+ class ::Email < DefinedHash
62
+ property :name, String
63
+ property :email, String
64
+ end
65
+
66
+ class ::Person
67
+ property :address, ::Address
68
+ property :emails, [::Email]
69
+ end
70
+ end
71
+
72
+ teardown do
73
+ Object.send(:remove_const, :Address)
74
+ end
75
+
76
+ suite do
77
+ setup :person, [
78
+ {:name => 'name', :address => { :name => 'home', :postcode => 'code' }},
79
+ {:name => 'name', :address => { 'name' => 'home', 'postcode' => 'code' }}
80
+ # {:name => 'name', :address => Address.new('name' => 'home', 'postcode' => 'code' )}
81
+ ] do |person|
82
+ @person = Person.new(person)
83
+ end
84
+
85
+
86
+ assert ":person has address name set correctly" do
87
+ @person[:address][:name] == 'home'
88
+ end
89
+
90
+ assert ":person has address postcode set correctly" do
91
+ @person[:address][:postcode] == 'code'
92
+ end
93
+ end
94
+ end
95
+
96
+ suite "Array properties", :depends_on => :new do
97
+ setup do
98
+ class ::Person
99
+ property :numbers, [String]
100
+ property :emails, [{:name => String}]
101
+ end
102
+ end
103
+
104
+ assert "An array property is assigned as an array" do
105
+ @person = Person.new(:numbers => %w{5551234})
106
+ equal_unordered(%w{5551234}, @person[:numbers])
107
+ end
108
+
109
+ assert "A non-array property is assigned as an array" do
110
+ @person = Person.new(:numbers => "5551234")
111
+ equal_unordered(%w{5551234}, @person[:numbers])
112
+ end
113
+
114
+ assert "A hash in array is added properly" do
115
+ @person = Person.new(:emails => { 'name' => 'gmail', :foo => 'bar'})
116
+ equal_unordered([{:name => 'gmail'}], @person[:emails])
117
+ end
118
+ assert "A hash in array is added properly" do
119
+ @person = Person.new(:emails => [{ 'name' => 'gmail', :foo => 'bar'}])
120
+ equal_unordered([{:name => 'gmail'}], @person[:emails])
121
+ end
122
+
123
+
124
+ end
125
+
126
+
127
+
128
+ suite "Testing validity", :depends_on => :new do
129
+ setup do
130
+ class ::Email < DefinedHash
131
+ property :name, String, :optional => true
132
+ property :email, String
133
+ end
134
+
135
+ class ::Number < DefinedHash
136
+ property :name, String, :optional => true
137
+ property :number, String
138
+ end
139
+
140
+ class ::Person
141
+ property :nick, String, :optional => true
142
+ property :email, Email, :optional => true
143
+ property :numbers, [Number], :optional => true
144
+ end
145
+ end
146
+
147
+ suite "invalid" do
148
+ setup :invalid_person, [
149
+ {:name => 'name'},
150
+ {:page => 'page'},
151
+ { :name => 'name', :page => 1 },
152
+ { :name => 1, :page => 'page' },
153
+ {:name => 'name', :page => 'page', :email => { :name => 'home'}},
154
+ {:name => 'name', :page => 'page', :email => { :email => 'a@example.com'}, :numbers => [{ :number => "555-1234" }, { }]},
155
+ {:name => 'name', :page => 'page', :nick => 2 }
156
+ ] do |invalid_person|
157
+ @person = Person.new(invalid_person)
158
+ end
159
+
160
+ assert ":invalid_person is invalid" do
161
+ @person.valid? == false
162
+ end
163
+ end
164
+
165
+ suite "valid" do
166
+ setup :valid_person, [
167
+ {:name => 'name', :page => 'page'},
168
+ {:name => 'name', :page => 'page', :email => { :name => 'home', :email => 'a@example.com'}},
169
+ {:name => 'name', :page => 'page', :email => { :email => 'a@example.com'}},
170
+ {:name => 'name', :page => 'page', :email => { :email => 'a@example.com'}, :numbers => [{ :number => "555-1234" }]},
171
+ {:name => 'name', :page => 'page', :nick => 'namz' }
172
+ ] do |valid_person|
173
+ @person = Person.new(valid_person)
174
+ end
175
+
176
+ assert ":valid_person is valid" do
177
+ @person.valid? == true
178
+ end
179
+ end
180
+ end
181
+
182
+ suite "merge!", :depends_on => :new do
183
+ setup do
184
+ @person = Person.new(:name => 'name', :page => 'p')
185
+ end
186
+
187
+ assert "properties are merged correctly from a hash" do
188
+ @person.merge!(:page => 'page', :foo => 'bar')
189
+ @person == Person.new(:name => 'name', :page => 'page')
190
+ end
191
+
192
+ assert "properties are merged correctly from a person" do
193
+ @person.merge!(Person.new(:page => 'page', :foo => 'bar'))
194
+ @person == Person.new(:name => 'name', :page => 'page')
195
+ end
196
+ end
197
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: defined_hash
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Jonathan Stott
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-07-05 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: hashidator
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: baretest
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: yard
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :development
62
+ version_requirements: *id003
63
+ description: Defined hashlike objects, with validations. For simple schema definitions and checking they're followed
64
+ email: jonathan.stott@gmail.com
65
+ executables: []
66
+
67
+ extensions: []
68
+
69
+ extra_rdoc_files:
70
+ - LICENSE
71
+ - README.rdoc
72
+ files:
73
+ - .document
74
+ - .gitignore
75
+ - LICENSE
76
+ - README.rdoc
77
+ - Rakefile
78
+ - VERSION
79
+ - lib/defined_hash.rb
80
+ - test/setup.rb
81
+ - test/suite/lib/hash.rb
82
+ has_rdoc: true
83
+ homepage: http://github.com/namelessjon/defined_hash
84
+ licenses: []
85
+
86
+ post_install_message:
87
+ rdoc_options:
88
+ - --charset=UTF-8
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ hash: 3
97
+ segments:
98
+ - 0
99
+ version: "0"
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ hash: 3
106
+ segments:
107
+ - 0
108
+ version: "0"
109
+ requirements: []
110
+
111
+ rubyforge_project:
112
+ rubygems_version: 1.3.7
113
+ signing_key:
114
+ specification_version: 3
115
+ summary: Defined hashlike objects, with validations
116
+ test_files:
117
+ - test/suite/lib/hash.rb
118
+ - test/setup.rb