property 0.9.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,101 @@
1
+ require 'property/role_module'
2
+ require 'property/stored_column'
3
+
4
+ module Property
5
+ # This module lets you use a custom class to store a set of property definitions inside
6
+ # the database. For the rest, this class behaves just like Role.
7
+ #
8
+ # Once this module is included, you need to set the has_many association to the class that
9
+ # contains the columns definitions with something like:
10
+ #
11
+ # has_many :stored_columns, :class_name => NameOfColumnsClass
12
+ module StoredRole
13
+ include RoleModule
14
+
15
+ module ClassMethods
16
+ def stored_columns_class(columns_class)
17
+ has_many :stored_columns, :class_name => columns_class
18
+ end
19
+ end
20
+
21
+ def self.included(base)
22
+ base.class_eval do
23
+ after_save :update_columns
24
+ validates_presence_of :name
25
+ extend ClassMethods
26
+
27
+ def self.new(arg, &block)
28
+ unless arg.kind_of?(Hash)
29
+ arg = {:name => arg}
30
+ end
31
+
32
+ if block_given?
33
+ obj = super(arg) do
34
+ # Dummy block to hide our special property declaration block
35
+ end
36
+
37
+ obj.property(&block)
38
+ else
39
+ obj = super(arg)
40
+ end
41
+
42
+ obj
43
+ end
44
+
45
+ # Initialize a new role with the given name
46
+ def initialize(*args)
47
+ super
48
+ initialize_role_module
49
+ end
50
+ end
51
+ end # included
52
+
53
+ # List all property definitions for the current role
54
+ def columns
55
+ load_columns_from_db unless @columns_from_db_loaded
56
+ super
57
+ end
58
+
59
+ def property
60
+ initialize_role_module unless @accessor_module
61
+ super
62
+ end
63
+
64
+
65
+ private
66
+ def load_columns_from_db
67
+ initialize_role_module
68
+ @columns_from_db_loaded = true
69
+ @original_columns = {}
70
+ stored_columns.each do |column|
71
+ @original_columns[column.name] = column
72
+ add_column(Property::Column.new(column.name, column.default, column.ptype, column.options))
73
+ end
74
+ end
75
+
76
+ def update_columns
77
+ @original_columns ||= {}
78
+ stored_column_names = @original_columns.keys
79
+ defined_column_names = self.column_names
80
+
81
+ new_columns = defined_column_names - stored_column_names
82
+ updated_columns = defined_column_names & stored_column_names
83
+ # Not needed: there is no way to remove a property right now
84
+ # deleted_columns = stored_column_names - defined_column_names
85
+
86
+ new_columns.each do |name|
87
+ ActiveRecord::Base.logger.warn "Creating #{name} column"
88
+ stored_columns.create(:name => name, :ptype => columns[name].type.to_s)
89
+ end
90
+
91
+ updated_columns.each do |name|
92
+ @original_columns[name].update_attributes(:name => name, :ptype => columns[name].type.to_s)
93
+ end
94
+
95
+ # Not needed: there is no way to remove a property right now
96
+ # deleted_columns.each do |name|
97
+ # @original_columns[name].destroy!
98
+ # end
99
+ end
100
+ end
101
+ end
data/property.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{property}
8
- s.version = "0.9.1"
8
+ s.version = "1.0.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Renaud Kern", "Gaspard Bucher"]
12
- s.date = %q{2010-03-20}
12
+ s.date = %q{2010-05-27}
13
13
  s.description = %q{Wrap model properties into a single database column and declare properties from within the model.}
14
14
  s.email = %q{gaspard@teti.ch}
15
15
  s.extra_rdoc_files = [
@@ -24,30 +24,41 @@ Gem::Specification.new do |s|
24
24
  "generators/property/property_generator.rb",
25
25
  "lib/property.rb",
26
26
  "lib/property/attribute.rb",
27
- "lib/property/behavior.rb",
27
+ "lib/property/base.rb",
28
28
  "lib/property/column.rb",
29
29
  "lib/property/core_ext/time.rb",
30
30
  "lib/property/db.rb",
31
31
  "lib/property/declaration.rb",
32
32
  "lib/property/dirty.rb",
33
+ "lib/property/error.rb",
33
34
  "lib/property/index.rb",
34
35
  "lib/property/properties.rb",
36
+ "lib/property/redefined_method_error.rb",
37
+ "lib/property/redefined_property_error.rb",
38
+ "lib/property/role.rb",
39
+ "lib/property/role_module.rb",
35
40
  "lib/property/schema.rb",
36
41
  "lib/property/serialization/json.rb",
37
42
  "lib/property/serialization/marshal.rb",
38
43
  "lib/property/serialization/yaml.rb",
44
+ "lib/property/stored_column.rb",
45
+ "lib/property/stored_role.rb",
39
46
  "property.gemspec",
40
47
  "test/fixtures.rb",
48
+ "test/shoulda_macros/index.rb",
49
+ "test/shoulda_macros/role.rb",
41
50
  "test/shoulda_macros/serialization.rb",
42
51
  "test/test_helper.rb",
43
52
  "test/unit/property/attribute_test.rb",
44
- "test/unit/property/behavior_test.rb",
53
+ "test/unit/property/base_test.rb",
45
54
  "test/unit/property/declaration_test.rb",
46
55
  "test/unit/property/dirty_test.rb",
47
56
  "test/unit/property/index_complex_test.rb",
48
57
  "test/unit/property/index_custom_test.rb",
49
58
  "test/unit/property/index_foreign_test.rb",
50
59
  "test/unit/property/index_simple_test.rb",
60
+ "test/unit/property/role_test.rb",
61
+ "test/unit/property/stored_role_test.rb",
51
62
  "test/unit/property/validation_test.rb",
52
63
  "test/unit/serialization/json_test.rb",
53
64
  "test/unit/serialization/marshal_test.rb",
@@ -61,16 +72,20 @@ Gem::Specification.new do |s|
61
72
  s.summary = %q{model properties wrap into a single database column}
62
73
  s.test_files = [
63
74
  "test/fixtures.rb",
75
+ "test/shoulda_macros/index.rb",
76
+ "test/shoulda_macros/role.rb",
64
77
  "test/shoulda_macros/serialization.rb",
65
78
  "test/test_helper.rb",
66
79
  "test/unit/property/attribute_test.rb",
67
- "test/unit/property/behavior_test.rb",
80
+ "test/unit/property/base_test.rb",
68
81
  "test/unit/property/declaration_test.rb",
69
82
  "test/unit/property/dirty_test.rb",
70
83
  "test/unit/property/index_complex_test.rb",
71
84
  "test/unit/property/index_custom_test.rb",
72
85
  "test/unit/property/index_foreign_test.rb",
73
86
  "test/unit/property/index_simple_test.rb",
87
+ "test/unit/property/role_test.rb",
88
+ "test/unit/property/stored_role_test.rb",
74
89
  "test/unit/property/validation_test.rb",
75
90
  "test/unit/serialization/json_test.rb",
76
91
  "test/unit/serialization/marshal_test.rb",
data/test/fixtures.rb CHANGED
@@ -4,6 +4,9 @@ class Employee < ActiveRecord::Base
4
4
  property.string 'first_name', :default => '', :index => true
5
5
  property.string 'last_name', :default => '', :index => true
6
6
  property.float 'age'
7
+
8
+ def method_in_parent
9
+ end
7
10
  end
8
11
 
9
12
  class Developer < Employee
@@ -78,6 +81,23 @@ begin
78
81
  t.string 'value'
79
82
  end
80
83
 
84
+ # multilingual index strings in employees
85
+ create_table 'i_ml_string_employees' do |t|
86
+ t.integer 'employee_id'
87
+ t.integer 'version_id'
88
+ t.string 'lang'
89
+ t.integer 'site_id'
90
+ t.string 'key'
91
+ t.string 'value'
92
+ end
93
+
94
+ # index strings in employees
95
+ create_table 'i_special_employees' do |t|
96
+ t.integer 'employee_id'
97
+ t.string 'key'
98
+ t.string 'value'
99
+ end
100
+
81
101
  # index integer in employees
82
102
  create_table 'i_integer_employees' do |t|
83
103
  t.integer 'employee_id'
@@ -99,11 +119,29 @@ begin
99
119
  t.string 'name'
100
120
  t.string 'other_name'
101
121
  end
122
+
123
+ # Database stored role
124
+ create_table 'roles' do |t|
125
+ t.integer 'id'
126
+ t.string 'name'
127
+ end
128
+
129
+ create_table 'columns' do |t|
130
+ t.integer 'id'
131
+ t.integer 'role_id'
132
+ t.string 'name'
133
+ # Property Type
134
+ t.string 'ptype'
135
+ # Indexed (we store an integer so that we can have multiple index types)
136
+ t.string 'index'
137
+ end
102
138
  end
103
139
  end
104
140
 
105
141
  ActiveRecord::Base.establish_connection(:adapter=>'sqlite3', :database=>':memory:')
106
- # ActiveRecord::Base.logger = Logger.new(STDOUT)
142
+ log_path = Pathname(__FILE__).dirname + '../log/test.log'
143
+ Dir.mkdir(log_path.dirname) unless File.exist?(log_path.dirname)
144
+ ActiveRecord::Base.logger = Logger.new(File.open(log_path, 'wb'))
107
145
  ActiveRecord::Migration.verbose = false
108
146
  #PropertyMigration.migrate(:down)
109
147
  PropertyMigration.migrate(:up)
@@ -0,0 +1,95 @@
1
+ module IndexMacros
2
+ class IndexedStringEmp < ActiveRecord::Base
3
+ set_table_name :i_string_employees
4
+ end
5
+
6
+ class MLIndexedStringEmp < ActiveRecord::Base
7
+ set_table_name :i_ml_string_employees
8
+ end
9
+
10
+ class IndexedIntegerEmp < ActiveRecord::Base
11
+ set_table_name :i_integer_employees
12
+ end
13
+
14
+ # Simple class
15
+ class Employee < ActiveRecord::Base
16
+ include Property
17
+
18
+ def index_reader(group_name)
19
+ if group_name.to_s == 'ml_string'
20
+ super.merge(:with => {'lang' => ['en', 'fr'], 'site_id' => '123'})
21
+ else
22
+ super
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ class Test::Unit::TestCase
29
+
30
+ def self.should_maintain_indices
31
+
32
+ context "assigned to an instance of Dummy" do
33
+ subject do
34
+ dummy = IndexMacros::Employee.new
35
+ dummy.has_role @poet
36
+ dummy
37
+ end
38
+
39
+ should 'use index_reader method' do
40
+ assert_equal Hash[:with=>{'lang'=>['en', 'fr'], 'site_id'=>'123'}, "employee_id"=>nil], subject.index_reader(:ml_string)
41
+ end
42
+
43
+ should 'create multilingual string indices on save' do
44
+ assert_difference('IndexMacros::MLIndexedStringEmp.count', 2) do
45
+ subject.poem = 'Hyperions Schicksalslied'
46
+ subject.save
47
+ end
48
+ end
49
+
50
+ should 'create integer indices on save' do
51
+ assert_difference('IndexMacros::IndexedIntegerEmp.count', 1) do
52
+ subject.year = 1770
53
+ subject.save
54
+ end
55
+ end
56
+
57
+ should 'not create blank indices on save' do
58
+ assert_difference('IndexMacros::IndexedStringEmp.count', 0) do
59
+ assert_difference('IndexMacros::IndexedIntegerEmp.count', 0) do
60
+ subject.save
61
+ end
62
+ end
63
+ end
64
+ end # An instance of Person
65
+ end
66
+
67
+ def self.should_not_maintain_indices
68
+
69
+ context "assigned to an instance of Dummy" do
70
+ subject do
71
+ dummy = IndexMacros::Employee.new
72
+ dummy.has_role @poet
73
+ dummy
74
+ end
75
+
76
+ should 'not create indices on save' do
77
+ assert_difference('IndexMacros::IndexedStringEmp.count', 0) do
78
+ assert_difference('IndexMacros::IndexedIntegerEmp.count', 0) do
79
+ subject.year = 1770
80
+ subject.poem = 'Hyperions Schicksalslied'
81
+ subject.save
82
+ end
83
+ end
84
+ end
85
+
86
+ should 'not create blank indices on save' do
87
+ assert_difference('IndexMacros::IndexedStringEmp.count', 0) do
88
+ assert_difference('IndexMacros::IndexedIntegerEmp.count', 0) do
89
+ subject.save
90
+ end
91
+ end
92
+ end
93
+ end # An instance of Person
94
+ end
95
+ end
@@ -0,0 +1,237 @@
1
+ class Test::Unit::TestCase
2
+ def self.should_store_property_definitions(klass)
3
+ subject { klass.new('Foobar') }
4
+
5
+ should 'allow string columns' do
6
+ subject.property.string('weapon')
7
+ column = subject.columns['weapon']
8
+ assert_equal 'weapon', column.name
9
+ assert_equal String, column.klass
10
+ assert_equal :string, column.type
11
+ end
12
+
13
+ should 'treat symbol keys as strings' do
14
+ subject.property.string(:weapon)
15
+ column = subject.columns['weapon']
16
+ assert_equal 'weapon', column.name
17
+ assert_equal String, column.klass
18
+ assert_equal :string, column.type
19
+ end
20
+
21
+ should 'allow integer columns' do
22
+ subject.property.integer('indestructible')
23
+ column = subject.columns['indestructible']
24
+ assert_equal 'indestructible', column.name
25
+ assert_equal Fixnum, column.klass
26
+ assert_equal :integer, column.type
27
+ end
28
+
29
+ should 'allow float columns' do
30
+ subject.property.float('boat')
31
+ column = subject.columns['boat']
32
+ assert_equal 'boat', column.name
33
+ assert_equal Float, column.klass
34
+ assert_equal :float, column.type
35
+ end
36
+
37
+ should 'allow datetime columns' do
38
+ subject.property.datetime('time_weapon')
39
+ column = subject.columns['time_weapon']
40
+ assert_equal 'time_weapon', column.name
41
+ assert_equal Time, column.klass
42
+ assert_equal :datetime, column.type
43
+ end
44
+
45
+ should 'allow default value option' do
46
+ subject.property.integer('force', :default => 10)
47
+ column = subject.columns['force']
48
+ assert_equal 10, column.default
49
+ end
50
+
51
+ should 'allow index option' do
52
+ subject.property.string('rolodex', :index => true)
53
+ column = subject.columns['rolodex']
54
+ assert column.indexed?
55
+ end
56
+
57
+ should 'return a list of indices on indices' do
58
+ subject.property.string('rolodex', :index => true)
59
+ subject.property.integer('foobar', :index => true)
60
+ assert_equal %w{integer string}, subject.indices.map {|i| i[0].to_s }.sort
61
+ end
62
+
63
+ context 'created with a Hash' do
64
+ subject { klass.new(:name => 'Foobar') }
65
+
66
+ should 'set name' do
67
+ assert_equal 'Foobar', subject.name
68
+ end
69
+ end
70
+
71
+ context 'created with a String Hash' do
72
+ subject { klass.new('name' => 'Foobar') }
73
+
74
+ should 'set name' do
75
+ assert_equal 'Foobar', subject.name
76
+ end
77
+ end
78
+ end # should_store_property_definitions
79
+
80
+ def self.should_insert_properties_on_has_role_poet
81
+ context 'added' do
82
+
83
+ context 'to a parent class' do
84
+ setup do
85
+ @parent = Class.new(ActiveRecord::Base) do
86
+ set_table_name :dummies
87
+ include Property
88
+ property.string 'name'
89
+ end
90
+
91
+ @klass = Class.new(@parent)
92
+ end
93
+
94
+ should 'propagate definitions to child' do
95
+ @parent.has_role @poet
96
+ assert_equal %w{name poem year}, @klass.schema.column_names.sort
97
+ end
98
+
99
+ should 'return true on has_role?' do
100
+ @parent.has_role @poet
101
+ assert @klass.has_role?(@poet)
102
+ end
103
+
104
+ should 'raise an exception if class contains same definitions' do
105
+ @parent.property.string 'poem'
106
+ assert_raise(Property::RedefinedPropertyError) { @parent.has_role @poet }
107
+ end
108
+
109
+ should 'not raise an exception on double inclusion' do
110
+ @parent.has_role @poet
111
+ assert_nothing_raised { @parent.has_role @poet }
112
+ end
113
+
114
+ should 'add accessor methods to child' do
115
+ subject = @klass.new
116
+ assert_raises(NoMethodError) { subject.poem = 'Poe'}
117
+ @parent.has_role @poet
118
+
119
+ assert_nothing_raised { subject.poem = 'Poe'}
120
+ end
121
+ end
122
+
123
+ context 'to a class' do
124
+ setup do
125
+ @klass = Class.new(ActiveRecord::Base) do
126
+ set_table_name :dummies
127
+ include Property
128
+ property.string 'name'
129
+ end
130
+ end
131
+
132
+ should 'insert definitions' do
133
+ @klass.has_role @poet
134
+ assert_equal %w{name poem year}, @klass.schema.column_names.sort
135
+ end
136
+
137
+ should 'return true on class has_role?' do
138
+ @klass.has_role @poet
139
+ assert @klass.has_role?(@poet)
140
+ end
141
+
142
+ end
143
+
144
+ context 'to an instance' do
145
+ subject { Developer.new }
146
+
147
+ setup do
148
+ subject.has_role @poet
149
+ end
150
+
151
+ should 'merge property definitions' do
152
+ assert_equal %w{age first_name language last_name poem year}, subject.schema.column_names.sort
153
+ end
154
+ end
155
+ end
156
+ end # should_insert_properties_on_has_role
157
+
158
+ def self.should_add_role_methods
159
+ context 'added' do
160
+ context 'to a parent class' do
161
+ setup do
162
+ @parent = Class.new(ActiveRecord::Base) do
163
+ set_table_name :dummies
164
+ include Property
165
+ property.string 'name'
166
+ end
167
+
168
+ @klass = Class.new(@parent)
169
+ end
170
+
171
+ should 'add role methods to child' do
172
+ subject = @klass.new
173
+ assert_raises(NoMethodError) { subject.muse }
174
+ @parent.has_role @poet
175
+
176
+ assert_nothing_raised { subject.muse }
177
+ end
178
+
179
+ should 'use role methods for defaults' do
180
+ subject = @klass.new
181
+ @parent.has_role @poet
182
+ assert subject.save
183
+ assert_equal 'I am your muse', subject.poem
184
+ end
185
+ end
186
+ end
187
+ end # should_add_role_methods
188
+
189
+ def self.should_take_part_in_used_list(has_defaults = true)
190
+
191
+ context 'added' do
192
+ context 'to a class' do
193
+ setup do
194
+ @klass = Class.new(ActiveRecord::Base) do
195
+ set_table_name :dummies
196
+ include Property
197
+ property.string 'name'
198
+ end
199
+
200
+ @klass.has_role @poet
201
+ end
202
+
203
+ subject do
204
+ @klass.new
205
+ end
206
+
207
+ should 'return true on instance has_role?' do
208
+ assert subject.has_role?(@poet)
209
+ end
210
+
211
+ should 'not return role without corresponding attributes' do
212
+ subject.attributes = {'name' => 'hello'}
213
+ assert_equal [@klass.schema.role], subject.used_roles
214
+ end
215
+
216
+ should 'return role with corresponding attributes' do
217
+ subject.attributes = {'poem' => 'hello'}
218
+ roles = @poet.kind_of?(Class) ? @poet.schema.roles : [@poet]
219
+ assert_equal roles, subject.used_roles
220
+ end
221
+
222
+ should 'not return role only with blank attributes' do
223
+ subject.attributes = {'name' => ''}
224
+ assert_equal [], subject.used_roles
225
+ end
226
+
227
+ if has_defaults
228
+ should 'return role with default attributes after validate' do
229
+ subject.valid?
230
+ roles = @poet.kind_of?(Class) ? @poet.schema.roles : [@poet]
231
+ assert_equal roles, subject.used_roles
232
+ end
233
+ end
234
+ end # to a class
235
+ end # added
236
+ end
237
+ end