property 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,80 @@
1
+ module Property
2
+ class Properties < Hash
3
+ attr_accessor :owner
4
+ include Property::DirtyProperties
5
+
6
+ def self.json_create(serialized)
7
+ self[serialized['data']]
8
+ end
9
+
10
+ def to_json(*args)
11
+ {
12
+ 'json_class' => self.class.name,
13
+ 'data' => Hash[self]
14
+ }.to_json(*args)
15
+ end
16
+
17
+ def []=(key, value)
18
+ if column = columns[key]
19
+ if value.blank?
20
+ if default = column.default
21
+ super(key, default)
22
+ else
23
+ delete(key)
24
+ end
25
+ else
26
+ super(key, column.type_cast(value.to_s))
27
+ end
28
+ else
29
+ super
30
+ end
31
+ end
32
+
33
+ # We need to write our own merge so that typecasting is called
34
+ def merge!(attributes)
35
+ raise TypeError.new("can't convert #{attributes.class} into Hash") unless attributes.kind_of?(Hash)
36
+ attributes.each do |key, value|
37
+ self[key] = value
38
+ end
39
+ end
40
+
41
+ def validate
42
+ columns = @owner.class.property_columns
43
+ column_names = @owner.class.property_column_names
44
+ errors = @owner.errors
45
+ no_errors = true
46
+
47
+ bad_keys = keys - column_names
48
+ missing_keys = column_names - keys
49
+
50
+ bad_keys.each do |key|
51
+ errors.add("#{key}", 'property is not declared')
52
+ end
53
+
54
+ missing_keys.each do |key|
55
+ column = columns[key]
56
+ if column.has_default?
57
+ self[key] = column.default
58
+ end
59
+ end
60
+
61
+ bad_keys.empty?
62
+ end
63
+
64
+ def compact!
65
+ #keys.each do |key|
66
+ # if self[key].nil?
67
+ # delete(key)
68
+ # end
69
+ #end
70
+ end
71
+
72
+ private
73
+ def write_attribute(key, value)
74
+ end
75
+
76
+ def columns
77
+ @columns ||= @owner.class.property_columns
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,38 @@
1
+ module Property
2
+ module Serialization
3
+ # Use JSON to encode properties. This is the serialization best option. It's
4
+ # the fastest and does not have any binary format issues. You just have to
5
+ # provide 'self.create_json' and 'to_json' methods for the classes you want
6
+ # to serialize.
7
+ module JSON
8
+ module ClassMethods
9
+ NATIVE_TYPES = [Hash, Array, Integer, Float, String, TrueClass, FalseClass, NilClass]
10
+
11
+ # Returns true if the given class can be serialized with JSON
12
+ def validate_property_class(klass)
13
+ if NATIVE_TYPES.include?(klass) ||
14
+ (klass.respond_to?('json_create') && klass.instance_methods.include?('to_json'))
15
+ true
16
+ else
17
+ raise TypeError.new("Cannot serialize #{klass}. Missing 'self.create_json' and 'to_json' methods.")
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.included(base)
23
+ base.extend ClassMethods
24
+ end
25
+
26
+ # Encode properties with Marhsal
27
+ def encode_properties(properties)
28
+ properties.to_json
29
+ end
30
+
31
+ # Decode Marshal encoded properties
32
+ def decode_properties(string)
33
+ ::JSON.parse(string)
34
+ end
35
+
36
+ end # JSON
37
+ end # Serialization
38
+ end # Property
@@ -0,0 +1,35 @@
1
+ module Property
2
+ module Serialization
3
+ # Use Marhsal to encode properties. Unless you have very good reasons
4
+ # to use Marshal, you should use the JSON serialization instead:
5
+ # * it's faster at reading text/date based objects
6
+ # * it's human readable
7
+ # * no corruption risk if the version of Marshal changes
8
+ # * it can be accessed by other languages then ruby
9
+ module Marshal
10
+ module ClassMethods
11
+ # Returns true if the given class can be serialized with Marshal
12
+ def validate_property_class(klass)
13
+ true
14
+ end
15
+ end
16
+
17
+ def self.included(base)
18
+ base.extend ClassMethods
19
+ end
20
+
21
+ # Encode properties with Marhsal
22
+ def encode_properties(properties)
23
+ # we limit dump depth to 0 (object only: no instance variables)
24
+ # we have to protect Marshal from serializing instance variables by making a copy
25
+ [::Marshal::dump(Properties[properties])].pack('m*')
26
+ end
27
+
28
+ # Decode Marshal encoded properties
29
+ def decode_properties(string)
30
+ ::Marshal::load(string.unpack('m')[0])
31
+ end
32
+
33
+ end # Marshal
34
+ end # Serialization
35
+ end # Property
@@ -0,0 +1,29 @@
1
+ module Property
2
+ module Serialization
3
+ # Use YAML to encode properties. This method is the slowest of all
4
+ # and you should use JSON if you haven't got good reasons not to.
5
+ module YAML
6
+ module ClassMethods
7
+ # Returns true if the given class can be serialized with YAML
8
+ def validate_property_class(klass)
9
+ true
10
+ end
11
+ end
12
+
13
+ def self.included(base)
14
+ base.extend ClassMethods
15
+ end
16
+
17
+ # Encode properties with YAML
18
+ def encode_properties(properties)
19
+ ::YAML.dump(properties)
20
+ end
21
+
22
+ # Decode properties from YAML
23
+ def decode_properties(string)
24
+ ::YAML::load(string)
25
+ end
26
+
27
+ end # Yaml
28
+ end # Serialization
29
+ end # Property
data/test/fixtures.rb ADDED
@@ -0,0 +1,57 @@
1
+
2
+ class Employee < ActiveRecord::Base
3
+ include Property
4
+ property.string 'first_name', :default => '', :indexed => true
5
+ property.string 'last_name', :default => '', :indexed => true
6
+ property.float 'age'
7
+ end
8
+
9
+ class Developer < Employee
10
+ property.string 'language'
11
+ end
12
+
13
+ class WebDeveloper < Developer
14
+
15
+ end
16
+
17
+ class Version < ActiveRecord::Base
18
+ attr_accessor :backup
19
+ include Property
20
+ property.string 'foo'
21
+ # Other way to declare a string
22
+ property do |p|
23
+ p.string 'tic', 'comment'
24
+ end
25
+ end
26
+
27
+ begin
28
+ class PropertyMigration < ActiveRecord::Migration
29
+ def self.down
30
+ drop_table "employees"
31
+ drop_table "versions"
32
+ end
33
+ def self.up
34
+ create_table "employees" do |t|
35
+ t.string "type"
36
+ t.text "properties"
37
+ end
38
+
39
+ create_table "versions" do |t|
40
+ t.string "properties"
41
+ t.string "title"
42
+ t.string "comment"
43
+ t.timestamps
44
+ end
45
+
46
+ create_table "dummies" do |t|
47
+ t.text "properties"
48
+ end
49
+ end
50
+ end
51
+
52
+ ActiveRecord::Base.establish_connection(:adapter=>'sqlite3', :database=>':memory:')
53
+ ActiveRecord::Migration.verbose = false
54
+ #PropertyMigration.migrate(:down)
55
+ PropertyMigration.migrate(:up)
56
+ ActiveRecord::Migration.verbose = true
57
+ end
@@ -0,0 +1,71 @@
1
+ class Test::Unit::TestCase
2
+
3
+ def self.should_encode_and_decode_properties
4
+ klass = self.name.gsub(/Test$/,'').constantize
5
+ context klass do
6
+ should 'respond to validate_property_class' do
7
+ assert klass.respond_to? :validate_property_class
8
+ end
9
+
10
+ [Property::Properties, String, Integer, Float].each do |a_class|
11
+ should "accept to serialize #{a_class}" do
12
+ assert klass.validate_property_class(a_class)
13
+ end
14
+ end
15
+ end
16
+
17
+ context "Instance of #{klass}" do
18
+ setup do
19
+ @obj = klass.new
20
+ end
21
+
22
+ should 'respond to :encode_properties' do
23
+ assert @obj.respond_to? :encode_properties
24
+ end
25
+
26
+ should 'respond to :decode_properties' do
27
+ assert @obj.respond_to? :decode_properties
28
+ end
29
+
30
+ context 'with Properties' do
31
+ setup do
32
+ @properties = Property::Properties['foo' => 'bar']
33
+ end
34
+
35
+ should 'encode Properties in string' do
36
+ assert_kind_of String, @obj.encode_properties(@properties)
37
+ end
38
+
39
+ should 'restore Properties from string' do
40
+ string = @obj.encode_properties(@properties)
41
+ properties = @obj.decode_properties(string)
42
+ assert_equal Property::Properties, properties.class
43
+ assert_equal @properties, properties
44
+ end
45
+
46
+ should 'not include instance variables' do
47
+ @properties.instance_eval do
48
+ @baz = 'some data'
49
+ @owner = klass.new
50
+ end
51
+ prop = @obj.decode_properties(@obj.encode_properties(@properties))
52
+ assert_nil prop.instance_variable_get(:@baz)
53
+ assert_nil prop.instance_variable_get(:@owner)
54
+ end
55
+ end
56
+
57
+ context 'with empty Properties' do
58
+ setup do
59
+ @properties = Property::Properties.new
60
+ end
61
+
62
+ should 'encode and decode' do
63
+ string = @obj.encode_properties(@properties)
64
+ properties = @obj.decode_properties(string)
65
+ assert_equal Property::Properties, properties.class
66
+ assert_equal @properties, properties
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,19 @@
1
+ require 'pathname'
2
+ $LOAD_PATH.unshift((Pathname(__FILE__).dirname + '..' + 'lib').expand_path)
3
+ require 'rubygems'
4
+ require 'test/unit'
5
+ require 'shoulda'
6
+ require 'active_record'
7
+ require 'property'
8
+ require 'shoulda_macros/serialization'
9
+
10
+ class Test::Unit::TestCase
11
+
12
+ def assert_attribute(value, attr_name, object=subject)
13
+ assert_equal value, object.send(attr_name)
14
+ assert_equal value, object[attr_name]
15
+ assert_equal value, object.attributes[attr_name]
16
+ assert_equal value, object.properties=erties[attr_name]
17
+ end
18
+
19
+ end
@@ -0,0 +1,334 @@
1
+ require 'test_helper'
2
+ require 'fixtures'
3
+ require 'benchmark'
4
+
5
+ class AttributeTest < Test::Unit::TestCase
6
+
7
+ ActiveRecord::Base.default_timezone = :utc
8
+ ENV['TZ'] = 'UTC'
9
+
10
+ context 'When including Property' do
11
+ should 'include Property::Attribute' do
12
+ assert Version.include?(Property::Attribute)
13
+ end
14
+
15
+ should 'include Property::Serialization::JSON' do
16
+ assert Version.include?(Property::Serialization::JSON)
17
+ end
18
+
19
+ should 'include Property::Dirty' do
20
+ assert Version.include?(Property::Dirty)
21
+ end
22
+
23
+ should 'include Property::Declaration' do
24
+ assert Version.include?(Property::Declaration)
25
+ end
26
+ end
27
+
28
+ context 'When defining new properties' do
29
+ should 'not allow symbols as keys' do
30
+ assert_raise(ArgumentError) do
31
+ Class.new(ActiveRecord::Base) do
32
+ include Property
33
+ property :foo, String
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ context 'When writing properties' do
40
+ subject { Version.new }
41
+
42
+ setup do
43
+ subject.properties = {'foo'=>'bar'}
44
+ end
45
+
46
+ context 'with properties=' do
47
+ should 'merge hash in current content' do
48
+ subject.properties = {'other' => 'value'}
49
+ assert_equal Hash['foo' => 'bar', 'other' => 'value'], subject.properties
50
+ end
51
+
52
+ should 'replace current values' do
53
+ subject.properties = {'foo' => 'baz'}
54
+ assert_equal Hash['foo' => 'baz'], subject.properties
55
+ end
56
+
57
+ should 'raise TypeError if new attributes is not a Hash' do
58
+ assert_raise(TypeError) { subject.properties = 'this a string' }
59
+ end
60
+ end
61
+
62
+ should 'with merge! should merge new attributes' do
63
+ subject.properties.merge!({'b'=>'bravo', 'c'=>'charlie'})
64
+ assert_equal Hash['foo' => 'bar', 'b' => 'bravo', 'c' => 'charlie'], subject.properties
65
+ end
66
+ end
67
+
68
+ context 'When writing attributes with hash access' do
69
+ subject { Version.new('foo' => 'bar') }
70
+
71
+ setup do
72
+ subject.properties['foo'] = 'babar'
73
+ end
74
+
75
+ should 'write a property into properties' do
76
+ assert_equal Hash['foo' => 'babar'], subject.properties
77
+ end
78
+
79
+ should 'save property in properties' do
80
+ subject.save
81
+ version = Version.find(subject.id)
82
+ assert_equal Hash['foo' => 'babar'], version.properties
83
+ end
84
+ end
85
+
86
+
87
+ context 'The properties of an object' do
88
+ subject { Version.new }
89
+
90
+ setup do
91
+ subject.properties={'foo'=>'bar', :tic=>:tac}
92
+ end
93
+
94
+ should 'be accessible with :properties method' do
95
+ assert_equal Hash['foo'=>'bar', :tic=>:tac], subject.properties
96
+ end
97
+
98
+ should 'be a kind of Hash' do
99
+ assert_kind_of Hash, subject.properties
100
+ end
101
+
102
+ should 'respond to delete' do
103
+ assert_equal 'bar', subject.properties.delete('foo')
104
+ assert_nil subject.properties['foo']
105
+ end
106
+
107
+ end
108
+
109
+ context 'Setting attributes' do
110
+ subject { Version.new }
111
+
112
+ setup do
113
+ subject.attributes = {'foo'=>'bar', 'title'=>'test', 'backup' => 'please'}
114
+ end
115
+
116
+ should 'set rails attributes' do
117
+ assert_equal 'test', subject.title
118
+ end
119
+
120
+ should 'set properties' do
121
+ assert_equal Hash['foo'=>'bar'], subject.properties
122
+ end
123
+
124
+ should 'call native methods' do
125
+ assert_equal 'please', subject.backup
126
+ end
127
+ end
128
+
129
+ context 'Initializing an object' do
130
+ subject { Version.new('foo'=>'bar', 'title'=>'test', 'backup' => 'please') }
131
+
132
+ should 'set rails attributes' do
133
+ assert_equal 'test', subject.title
134
+ end
135
+
136
+ should 'set properties' do
137
+ assert_equal Hash['foo'=>'bar'], subject.properties
138
+ end
139
+
140
+ should 'call native methods' do
141
+ assert_equal 'please', subject.backup
142
+ end
143
+ end
144
+
145
+ context 'Updating attributes' do
146
+ setup do
147
+ version = Version.create('title' => 'first', 'tic' => 'tac')
148
+ @version = Version.find(version.id)
149
+ assert subject.update_attributes('foo'=>'bar', 'title'=>'test', 'backup' => 'please')
150
+ end
151
+
152
+ subject { @version }
153
+
154
+ should 'update rails attributes' do
155
+ assert_equal 'test', subject.title
156
+ end
157
+
158
+ should 'update properties' do
159
+ assert_equal Hash['tic' => 'tac', 'foo'=>'bar'], subject.properties
160
+ end
161
+
162
+ should 'call native methods' do
163
+ assert_equal 'please', subject.backup
164
+ end
165
+ end
166
+
167
+ context 'Saving attributes' do
168
+ setup do
169
+ version = Version.create('title'=>'test', 'foo' => 'bar', 'backup' => 'please')
170
+ @version = Version.find(version.id)
171
+ end
172
+
173
+ subject { @version }
174
+
175
+ should 'save rails attributes' do
176
+ assert_equal 'test', subject.title
177
+ end
178
+
179
+ should 'save properties' do
180
+ assert_equal 'bar', subject.prop['foo']
181
+ end
182
+
183
+ should 'destroy' do
184
+ assert subject.destroy
185
+ assert subject.frozen?
186
+ end
187
+
188
+ should 'delete' do
189
+ assert subject.delete
190
+ assert subject.frozen?
191
+ end
192
+ end
193
+
194
+ context 'Saving empty attributes' do
195
+ subject { Version.new('foo' => 'bar') }
196
+
197
+ setup do
198
+ subject.prop.delete('foo')
199
+ subject.save
200
+ end
201
+
202
+ should 'save nil in database' do
203
+ assert_nil subject['properties']
204
+ end
205
+
206
+ should 'save nil when last property is removed' do
207
+ subject = Version.create('foo' => 'bar', 'tic' => 'tac')
208
+ subject.attributes = {'foo' => nil}
209
+ subject.update_attributes('foo' => nil)
210
+ assert_equal ['tic'], subject.properties.keys
211
+ subject.properties.delete('tic')
212
+ subject.save
213
+ assert_nil subject['properties']
214
+ end
215
+ end
216
+
217
+ context 'Saving without changes to properties' do
218
+ setup do
219
+ version = Version.create('title' => 'test', 'foo' => 'bar')
220
+ @version = Version.find(version.id)
221
+ subject.update_attributes('title' => 'updated')
222
+ end
223
+
224
+ subject { @version }
225
+
226
+ should 'not alter properties' do
227
+ assert_equal Hash['foo' => 'bar'], subject.properties
228
+ end
229
+ end
230
+
231
+ context 'Find' do
232
+ subject { Version.create('title'=>'find me', 'foo' => 'bar') }
233
+
234
+ should 'find by id' do
235
+ version = Version.find(subject)
236
+ assert_equal 'bar', version.prop['foo']
237
+ end
238
+ end
239
+
240
+ context 'A modified version receiving :reload_properties' do
241
+ should 'return properties stored in database' do
242
+ subject = Version.create('title'=>'find me', 'foo' => 'bar')
243
+ subject.prop['foo'] = 'Babar'
244
+ assert_equal 'Babar', subject.prop['foo']
245
+ subject.reload_properties!
246
+ assert_equal 'bar', subject.prop['foo']
247
+ end
248
+ end
249
+
250
+ context 'Type cast' do
251
+ DataType = Class.new(ActiveRecord::Base) do
252
+ set_table_name 'dummies'
253
+ include Property
254
+ property do |p|
255
+ p.string 'mystring'
256
+ p.integer 'myinteger'
257
+ p.float 'myfloat'
258
+ p.datetime 'mytime'
259
+ end
260
+ end
261
+
262
+ should 'save and read String' do
263
+ subject = DataType.create('mystring' => 'some string')
264
+ subject.reload
265
+ assert_kind_of String, subject.prop['mystring']
266
+ end
267
+
268
+ should 'save and read Integer' do
269
+ subject = DataType.create('myinteger' => 123)
270
+ subject.reload
271
+ assert_kind_of Integer, subject.prop['myinteger']
272
+ end
273
+
274
+ should 'save and read Float' do
275
+ subject = DataType.create('myfloat' => 12.3)
276
+ subject.reload
277
+ assert_kind_of Float, subject.prop['myfloat']
278
+ end
279
+
280
+ should 'save and read Time' do
281
+ subject = DataType.create('mytime' => Time.new)
282
+ subject.reload
283
+ assert_kind_of Time, subject.prop['mytime']
284
+ end
285
+
286
+ context 'from a String' do
287
+ should 'parse integer values' do
288
+ subject = DataType.create('myinteger' => '123')
289
+ subject.reload
290
+ assert_kind_of Integer, subject.prop['myinteger']
291
+ assert_equal 123, subject.prop['myinteger']
292
+ end
293
+
294
+ should 'parse float values' do
295
+ subject = DataType.create('myfloat' => '12.3')
296
+ subject.reload
297
+ assert_kind_of Float, subject.prop['myfloat']
298
+ assert_equal 12.3, subject.prop['myfloat']
299
+ end
300
+
301
+ should 'parse time values' do
302
+ subject = DataType.create('mytime' => '2010-02-10 21:21')
303
+ subject.reload
304
+ assert_kind_of Time, subject.prop['mytime']
305
+ assert_equal Time.utc(2010,02,10,21,21), subject.prop['mytime']
306
+ end
307
+
308
+ context 'in the model' do
309
+ should 'parse integer values' do
310
+ subject = DataType.new
311
+ subject.prop['myinteger'] = '123'
312
+ assert_kind_of Integer, subject.prop['myinteger']
313
+ assert_equal 123, subject.prop['myinteger']
314
+ end
315
+
316
+ should 'parse float values' do
317
+ subject = DataType.new
318
+ subject.prop['myfloat'] = '12.3'
319
+ assert_kind_of Float, subject.prop['myfloat']
320
+ assert_equal 12.3, subject.prop['myfloat']
321
+ end
322
+
323
+ should 'parse time values' do
324
+ subject = DataType.new
325
+ subject.prop['mytime'] = '2010-02-10 21:21'
326
+ assert_kind_of Time, subject.prop['mytime']
327
+ assert_equal Time.utc(2010,02,10,21,21), subject.prop['mytime']
328
+ end
329
+ end
330
+ end
331
+ end
332
+
333
+
334
+ end