property 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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